Подтвердить что ты не робот

Порядок операций в понимании словаря

Я наткнулся на следующую интересную конструкцию:

при условии, что у вас есть список списков:

my_list = [['captain1', 'foo1', 'bar1', 'foobar1'], ['captain2', 'foo2', 'bar2', 'foobar2'], ...]

и вы хотите создать dict из них с элементами 0 -index, являющимися ключами. Удобный способ сделать это:

my_dict = {x.pop(0): x for x in my_list}
# {'captain1': ['foo1', 'bar1', 'foobar1'], ...}

Как кажется, pop предшествует присвоению списка x как значения, и поэтому 'captain' не отображается в значениях (он уже выскочил)

Теперь давайте сделаем еще один шаг и попытаемся получить такую ​​структуру, как:

# {'captain1': {'column1': 'foo1', 'column2': 'bar1', 'column3': 'foobar1'}, ...}

Для этой задачи я написал следующее:

my_headers = ['column1', 'column2', 'column3']
my_dict = {x.pop(0): {k: v for k, v in zip(my_headers, x)} for x in my_list}

но это возвращает:

# {'captain1': {'col3': 'bar1', 'col1': 'captain1', 'col2': 'foo1'}, 'captain2': {'col3': 'bar2', 'col1': 'captain2', 'col2': 'foo2'}}

поэтому pop в этом случае происходит после создания внутреннего словаря (или, по крайней мере, после zip).

Как это может быть? Как это работает?

Вопрос не в том, как это сделать, а в том, почему это поведение видно.

Я использую версию Python 3.5.1.

4b9b3361

Ответ 1

Примечание. Начиная с Python 3.8 и PEP 572, это было изменено, и ключи сначала анализируются.


tl;dr До Python 3.7: несмотря на то, что Python сначала оценивает значения (правая часть выражения), это выглядит как ошибка в (C) Python в соответствии с справочным руководством и грамматика и PEP о понимании слова.

Хотя ранее это было исправлено для отображений в словаре, где значения снова оценивались перед ключами, в патч не были внесены изменения, включающие в себя диктовки. Это требование было также упомянуто одним из разработчиков ядра в ветке списка рассылки, обсуждающей эту же тему.

Согласно справочному руководству, Python оценивает выражения слева направо и присваивания справа налево; Дикт-понимание - это действительно выражение, содержащее выражения, а не присваивание *:

{expr1: expr2 for ...}

где, согласно соответствующему правилу grammar, можно ожидать, что expr1: expr2 будет оцениваться аналогично тому, что он делает на дисплеях. Таким образом, оба выражения должны следовать определенному порядку, expr1 должен оцениваться до expr2 (и, если expr2 содержит собственные выражения, они также должны оцениваться слева направо.)

PEP на dict-comps дополнительно заявляет, что следующее должно быть семантически эквивалентным:

Семантика диктовок может быть на самом деле продемонстрирована в стандартный Python 2.2, передавая понимание списка встроенному конструктор словаря:

>>> dict([(i, chr(65+i)) for i in range(4)])

семантически эквивалентно:

>>> {i : chr(65+i) for i in range(4)}

где кортеж (i, chr(65+i)) оценивается слева направо, как и ожидалось.

Изменение этого поведения в соответствии с правилами для выражений может привести к несогласованности при создании dict, конечно. Понимание словаря и цикл for с присваиваниями приводят к другому порядку оценки, но это хорошо, поскольку он просто следует правилам.

Хотя это не является серьезной проблемой, ее следует устранить (или правило оценки, или документы), чтобы устранить неоднозначность ситуации.

* Внутренне это приводит к присваиванию объекту словаря, но это не должно нарушать поведение, которое должны иметь выражения. Пользователи ожидают, что выражения должны вести себя так, как указано в справочном руководстве.


Как указали другие авторы, поскольку вы выполняете мутирующее действие в одном из выражений, вы выбрасываете любую информацию о том, что оценивается первым; использование вызовов print, как это сделал Дункан, проливает свет на то, что сделано.

Функция, помогающая показать несоответствие:

def printer(val):
    print(val, end=' ')
    return val

(Исправлено) отображение словаря:

>>> d = {printer(0): printer(1), printer(2): printer(3)}
0 1 2 3

(нечетное) словарное понимание:

>>> t = (0, 1), (2, 3)
>>> d = {printer(i):printer(j) for i,j in t}
1 0 3 2

и да, это относится конкретно к C Python. Я не знаю, как другие реализации оценивают этот конкретный случай (хотя все они должны соответствовать Справочному руководству по Python.)

Копаться по источнику всегда приятно (и вы также найдете скрытые комментарии, описывающие поведение), поэтому давайте заглянем в compiler_sync_comprehension_generator файла compile.c:

case COMP_DICTCOMP:
    /* With 'd[k] = v', v is evaluated before k, so we do
       the same. */
    VISIT(c, expr, val);
    VISIT(c, expr, elt);
    ADDOP_I(c, MAP_ADD, gen_index + 1);
    break;

это может показаться достаточно веской причиной, и, если это так, следует вместо этого классифицировать как ошибку документации.

В одном из быстрых тестов я переключил эти операторы (VISIT(c, expr, elt); посещался первым), а также переключил соответствующий порядок в MAP_ADD (который используется для dict-comps):

TARGET(MAP_ADD) {
    PyObject *value = TOP();   # was key 
    PyObject *key = SECOND();  # was value
    PyObject *map;
    int err;

приводит к оценке, которую можно ожидать на основе документов, с ключом, оцененным до значения. (Не для их асинхронных версий, требуется другой переключатель.)


Я оставлю комментарий по этому вопросу и сообщу, когда и когда кто-нибудь ответит мне.

Создан Выпуск 29652 - Исправлен порядок оценки ключей/значений при распознавании слов на трекере. Обновит вопрос, когда будет достигнут прогресс.

Ответ 2

Как кажется, поп предшествует присваиванию списка x в качестве значения    и поэтому "капитан" не отображается в значениях (он уже    совал)

Нет, порядок, в котором это происходит, не имеет значения. Вы мутируете список, чтобы вы увидели измененный список после поп, где бы вы его ни использовали. Обратите внимание, что в целом вы, вероятно, не хотите этого делать, поскольку вы уничтожите исходный список. Даже если это не имеет значения, на этот раз это ловушка для неосторожного в будущем.

В обоих случаях сначала вычисляется стоимость, а затем соответствующий ключ. Это просто, что в первом случае это не имеет значения, в то время как во втором.

Это можно легко увидеть:

>>> def foo(a): print("foo", a)
... 
>>> def bar(a): print("bar", a)
... 
>>> { foo(a):bar(a) for a in (1, 2, 3) }
('bar', 1)
('foo', 1)
('bar', 2)
('foo', 2)
('bar', 3)
('foo', 3)
{None: None}
>>> 

Обратите внимание, что вы не должны писать код, который зависит от значений, которые сначала оцениваются: поведение может измениться в будущих версиях (в некоторых местах было сказано, что в Python 3.5 было изменено, но на самом деле это не похоже на то, что случай).

Простейший способ сделать это, что позволяет избежать изменения исходной структуры данных:

my_dict = {x[0]: x[1:] for x in my_list}

Или ваш второй пример:

my_headers = ['column1', 'column2', 'column3']
my_dict = {x[0]: {k: v for k, v in zip(my_headers, x[1:])} for x in my_list}

Чтобы ответить на комментарии: zip использует исходный x, потому что он оценивается до pop, но он использует содержимое списка для создания нового списка, поэтому никаких последующих изменений в списке не видно в результате. Первое понимание также использует исходное значение x в качестве значения, но затем оно мутирует список, поэтому значение все еще видит исходный список и, следовательно, мутацию.

Ответ 3

Как я уже сказал в комментарии, потому что в понимании словаря python сначала оценивает значение. И как более пифонический подход, вы можете использовать распаковку переменных для этой задачи, вместо того, чтобы выскакивать из списка на каждой итерации:

In [32]: my_list = [['captain1', 'foo1', 'bar1', 'foobar1'], ['captain2', 'foo2', 'bar2', 'foobar2']]

In [33]: {frist: {"column{}".format(i): k for i, k in enumerate(last, 1)} for frist, *last in my_list}
Out[33]: 
{'captain2': {'column3': 'foobar2', 'column1': 'foo2', 'column2': 'bar2'},
 'captain1': {'column3': 'foobar1', 'column1': 'foo1', 'column2': 'bar1'}}

Что касается странного поведения python при оценке ключей и значений в понимании словаря, то после некоторых экспериментов я понял, что это поведение как-то разумно, а не является ошибкой.

Я буду тормозить свое впечатление в следующих частях:

  • В выражении назначения python сначала оценивает правую сторону. from doc:

    Python оценивает выражения слева направо. Обратите внимание, что при оценке присваивания правая часть оценивается перед левой стороной.

  • Понимание словаря является выражением и будет оцениваться слева направо, но поскольку есть назначение под капотом, после перевода его на python. сначала будет оценено значение, которое является правой стороной.

    например, следующее понимание:

    {b.pop(0): b.pop(0) for _ in range(1)} эквивалентно следующему фрагменту:


def dict_comprehension():
    the_dict = {}
    for _ in range(1):
        the_dict[b.pop(0)] = b.pop(0)
    return the_dict

Вот несколько примеров:

In [12]: b = [4, 0]

# simple rule : Python evaluates expressions from left to right.
In [13]: [[b.pop(0), b.pop(0)] for _ in range(1)]
Out[13]: [[4, 0]]

In [14]: b = [4, 0]
# while evaluating an assignment (aforementioned rule 1), the right-hand side is evaluated before the left-hand side.
In [15]: {b.pop(0): b.pop(0) for _ in range(1)}
Out[15]: {0: 4}

In [16]: b = [4, 0]
# This is not a dictionary comprehension and will be evaluated left to right.
In [17]: {b.pop(0): {b.pop(0) for _ in range(1)}}
Out[17]: {4: {0}}

In [18]: b = [4, 0]
# This is not a dictionary comprehension and will be evaluated left to right.
In [19]: {b.pop(0): b.pop(0) == 0}
Out[19]: {4: True}

In [20]: b = [4, 0]
# dictionary comprehension.
In [21]: {b.pop(0): {b.pop(0) for _ in range(1)} for _ in range(1)}
Out[21]: {0: {4}}

Что касается несоответствия между фактом (или лучше сказать абстракцию), то понимание словаря является выражением и должно оцениваться слева направо (на основе документации на python) с наблюдаемым поведением я думаю, что это на самом деле проблема и незрелость документации python, а не ошибка в коде python. Поскольку вообще не разумно изменять функциональность из-за наличия согласованной документации без каких-либо исключений.

Ответ 4

На самом деле ваше наблюдение не требует специального заказа операции. Причина в том, что x.pop(0) изменяет объект x. Итак, независимо от того, оцениваете ли вы значение (x) до или после того, как ключ (x.pop(0)) не имеет значения в этом случае.

В любом случае, я не думаю, что спецификация языка python предписывает определенный порядок операций, а это означает, что вы не должны полагаться на какой-то конкретный порядок.

Фактически стандартная реализация выполняется для оценки значения до того, как он оценит ключ, но там нигде в стандарте, где это указано. Единственная гарантия заключается в том, что пары ключ-значение оцениваются в порядке итерации и вставляются в этом порядке.