在 **kwargs 中使用 OrderedDict

36

是否可能将OrderedDict实例传递给使用**kwargs语法的函数并保留顺序?

我想要做的是:

def I_crave_order(**kwargs):
    for k, v in kwargs.items():
        print k, v

example = OrderedDict([('first', 1), ('second', 2), ('third', -1)])

I_crave_order(**example)
>> first 1
>> second 2
>> third -1

但实际结果是:

>> second 2
>> third -1
>> first 1

即,典型的随机字典排序。

我有其他用途,显式地设置顺序是好的,因此我希望保留**kwargs而不仅仅将有序字典作为普通参数传递。


感谢您提供详细的答案。我最终所做的是允许一个可选的第一个参数(有序字典),如果提供了该参数,则优先使用它而不是 **kwargs。非常有用的信息。 - theodox
另请参见:https://duckduckgo.com/?q=pep468 - dreftymac
3个回答

35
自 Python 3.6 起,关键字参数的顺序被保留。在此之前的版本,这是不可能的,因为 OrderedDict 会被转换成一个 dict
首先要认识到的是,在传入**example的值不会自动成为**kwargs中的值。考虑这种情况,其中kwargs只会有example的一部分:
def f(a, **kwargs):
    pass
example = {'a': 1, 'b': 2}
f(**example)

在这种情况下,它将具有比示例中更多的值:
example = {'b': 2}
f(a=1, c=3, **example)

甚至可能完全没有重叠:

example = {'a': 1}
f(b=2, **example)

因此,你所要求的并没有实际意义。

尽管如此,如果有一种方式可以指定您需要的有序 **kwargs ,无论关键字来自何处——按它们出现的顺序显式关键字参数,然后是所有来自 example **example 项目按 example 中的顺序排列(如果 example 是一个 dict ,则可能是任意的,但如果它是一个 OrderedDict ,则也可能是有意义的)。

事实证明,定义所有微小的细节并保持性能可接受比听起来要困难得多。请参见PEP 468和链接的主题以讨论这个想法。它似乎在这次版本中已经停滞不前,但是如果有人拿起它并支持它(并为人们编写一个参考实现——这取决于可以从C API访问的 OrderedDict ,但希望在3.5+中会有),我认为它最终将被纳入语言之中。


顺便提一下,注意如果这是可能的话,它几乎肯定会在OrderedDict的构造函数中使用。但是如果你这样做,你只是冻结了某个任意顺序作为永久顺序。
>>> d = OrderedDict(a=1, b=2, c=3)
OrderedDict([('a', 1), ('c', 3), ('b', 2)])

同时,你有哪些选项呢?
显然的选择是将example作为普通参数传递而不是解包它。
def f(example):
    pass
example = OrderedDict([('a', 1), ('b', 2)])
f(example)

当然,你也可以使用*args并将项目作为元组传递,但通常会更加丑陋。

可能在PEP链接的线程中还有其他解决方法,但实际上,它们都不会比这个更好。 (除了......如果我没记错的话,李浩一提出了一种基于他的MacroPy的解决方案,以传递保持顺序的关键字参数,但我不记得细节了。总体而言,如果您愿意使用MacroPy并编写不完全像Python的代码,那么MacroPy解决方案非常棒,但这并不总是合适的......)


+1 细节,以及我新的最爱代码审查回复“所以,你要求的并没有太多意义。”。感谢您的帮助。 - cod3monk3y
感谢提供这些好链接。PEP是一个有趣的话题,因为它表示kwargs保证是一个保持插入顺序的映射,但接下来的PEP表示新的dict实现是保持顺序的,但这应该被视为一种实现特性。在3.7版本中,当我检查kwargs值的类型时,它是一个字典。所以我猜kwargs PEP的当前实现懒惰地依赖于当前的dict实现来保持顺序。 - MB.
1
@MB。从Python 3.7开始,字典的有序性已经成为语言特性而不是实现细节。这是一个有争议的变化,它肯定让我感到惊讶,但它确实发生了。 - Imperishable Night

20

这现在是Python 3.6中的默认设置

Python 3.6.0a4+ (default:d43f819caea7, Sep  8 2016, 13:05:34)
>>> def func(**kw): print(kw.keys())
...
>>> func(a=1, b=2, c=3, d=4, e=5)
dict_keys(['a', 'b', 'c', 'd', 'e'])   # expected order

正如其他答案所指出的那样,在此之前是不可能做到的。


3
当Python在签名中遇到**kwargs结构时,它期望kwargs是一个"映射",这意味着两件事:(1)能够调用kwargs.keys()获得映射包含的键的可迭代对象;(2)kwargs.__getitem__(key)可以为由keys()返回的每个键调用,并且生成的值是与该键相关联的所需值。
在内部,Python会将任何映射"转换"为字典,就像这样:
**kwargs -> {key:kwargs[key] for key in kwargs.keys()}

如果您认为kwargs已经是dict,那么这看起来可能有点傻 - 因为没有理由从传入的dict构造一个完全等效的dict。但是当kwargs不一定是dict时,将其内容带入合适的默认数据结构就很重要了,以便执行参数解包的代码始终知道自己正在处理什么。
所以,您可以干预特定数据类型的解包方式,但由于为了实现统一的参数解包协议而转换为dict,所以无法保证解包参数的顺序(因为dict不会跟踪元素添加的顺序)。如果Python语言将**kwargs转换为OrderedDict而不是dict(这意味着关键字参数的键的顺序将是它们被遍历的顺序),那么通过传递OrderedDict或一些其他数据结构使得keys()保持某种排序,您可以期望某些参数的顺序排列。只是因为实现中选择了dict作为标准,而不是其他类型的映射结构,所以出现了这种怪癖。
以下是一个可以“解包”的类的愚蠢示例,但它始终将所有解包的值视为42(尽管它们并不真正如此)。
class MyOrderedDict(object):
    def __init__(self, odict):
        self._odict = odict

    def __repr__(self):
        return self._odict.__repr__()

    def __getitem__(self, item):
        return 42

    def __setitem__(self, item, value):
        self._odict[item] = value

    def keys(self):
        return self._odict.keys()

然后定义一个函数来打印解包后的内容:
def foo(**kwargs):
    for k, v in kwargs.iteritems():
        print k, v

创建一个变量并尝试运行它:

In [257]: import collections; od = collections.OrderedDict()

In [258]: od['a'] = 1; od['b'] = 2; od['c'] = 3;

In [259]: md = MyOrderedDict(od)

In [260]: print md
OrderedDict([('a', 1), ('b', 2), ('c', 3)])

In [261]: md.keys()
Out[261]: ['a', 'b', 'c']

In [262]: foo(**md)
a 42
c 42
b 42

这里的定制键值对传递(在这里,始终返回 42)是您在 Python 中调整如何使用 **kwargs 的能力的极限。

更多关于如何调整 *args 获取解包的灵活性。有关更多信息,请参见此问题:Does argument unpacking use iteration or item-getting?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接