Использование OrderedDict в **kwargs

Можно ли передать экземпляр 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

т. е. типичный случайный порядок dict.

У меня есть другие способы использования, когда явное задание порядка полезно, поэтому я хочу сохранить **kwargs, а не просто передавать OrderedDict в качестве обычного аргумента.


person theodox    schedule 05.11.2014    source источник
comment
Спасибо за подробный ответ. В итоге я разрешил необязательный первый аргумент (упорядоченный словарь), который будет использоваться вместо **kwargs, если он будет предоставлен. Отличная информация, однако   -  person theodox    schedule 05.11.2014
comment
См. также: duckduckgo.com/?q=pep468   -  person dreftymac    schedule 06.04.2017


Ответы (3)


Начиная с Python 3.6 порядок аргументов ключевого слова сохраняется. До версии 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 было dict, но также могло бы иметь смысл, если бы это было OrderedDict).

Определить все мелкие детали и сохранить приемлемую производительность оказывается сложнее, чем кажется. См. PEP 468 и связанные темы для обсуждения этой идеи. Похоже, на этот раз он застопорился, но если кто-то поднимет его и поддержит (и напишет эталонную реализацию для людей, с которой можно поиграть, что зависит от OrderedDict, доступного из C API, но, надеюсь, это будет в версии 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, могут быть какие-то другие обходные пути, но на самом деле ни один из них не будет лучше этого. (За исключением… IIRC, Ли Хаойи предложил решение, основанное на его MacroPy, для передачи аргументов ключевых слов, сохраняющих порядок. , но я не помню подробностей. Решения MacroPy в целом потрясающие, если вы хотите использовать MacroPy и писать код, который не совсем читается как Python, но это не всегда уместно…)

person abarnert    schedule 05.11.2014
comment
+1 за подробности и мой новый любимый ответ на проверку кода So, what you're asking for doesn't really make sense.. Спасибо за это. - person cod3monk3y; 03.03.2019
comment
Спасибо за хорошие ссылки. Pep интересен, потому что он говорит, что **kwargs гарантированно является отображением, сохраняющим порядок вставки, но следующий шаг говорит, что новая реализация dict сохраняет порядок, но это следует рассматривать как особенность реализации. В 3.7, когда я проверяю тип значения **kwargs, это dict. Поэтому я предполагаю, что текущая реализация kwargs pep лениво зависит от текущей реализации dict, которая будет сохранять порядок. - person MB.; 04.04.2019
comment
@МБ. Начиная с Python 3.7, сохранение порядка в dict стало особенностью языка, а не деталью реализации. Это спорное изменение, и оно меня, конечно, удивило, но оно произошло. - person Imperishable Night; 04.06.2019

Теперь это значение по умолчанию в 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

Это невозможно сделать раньше, как указано в других ответах.

person damio    schedule 09.09.2016

Когда Python встречает конструкцию **kwargs в подписи, он ожидает, что kwargs будет «отображением», что означает две вещи: (1) возможность вызывать kwargs.keys() для получения итерации ключей, содержащихся в отображении, и (2) что kwargs.__getitem__(key) можно вызывать для каждого ключа в итерации, возвращаемой keys(), и что результирующее значение является желаемым для этого ключа.

Внутри Python затем «преобразует» любое сопоставление в словарь, примерно так:

**kwargs -> {key:kwargs[key] for key in kwargs.keys()}

Это выглядит немного глупо, если вы думаете, что kwargs уже является 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) — это степень вашей способности возиться с тем, как **kwargs работает в Python.

Немного больше гибкости в настройке того, как *args распаковывается. Подробнее об этом см. в этом вопросе: ‹ Использует ли распаковка аргумента итерацию или элемент -получение? >.

person ely    schedule 05.11.2014