Почему этот простой код Python приводит к пустому словарю?

Почему этот простой код Python приводит к пустому словарю?

from collections import defaultdict
dict1 = defaultdict(list)
dict1['old_key'] = [{'name': 'A'}]
dict1['old_key'].extend(dict1.pop('old_key'))

print(dict1)

Мое понимание того, как это должно было работать:

Метод pop вернет список, связанный с old_key, который будет передан методу расширения. Метод расширения снова установит old_key; поскольку к этому моменту он не будет существовать, он инициализирует его пустым списком и расширит этот пустой список списком, полученным из метода pop.

Итак, это должно было привести к {'old_key': [{'name': 'A'}]}, но я получаю следующее: {}

Контекст фактического кода:

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

Однако иногда случается так, что новый ключ, сгенерированный на основе некоторых условий, совпадает со старым ключом, и код, имеющий расширение и всплывающее окно в одной строке, ведет себя странно.

Конечно, я могу это исправить, либо разбив на 2 строки: сначала вытолкнуть и сохранить результат в значении, а затем расширить.

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

Но я хочу понять, что вызывает странное поведение кода.

Возможный дубликат stackoverflow.com/questions/6777485/…

tripleee 10.05.2024 18:49
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
1
58
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

выходные данные будут пустым словарем, поскольку список, связанный с ключом «old_key», был перемещен в новый ключ «old_key», а затем исходный ключ «old_key» удален. Следовательно, после этих операций словарь dict1 будет пустым.

Это не совсем правильно, поскольку список не «перемещается на новый ключ». Ключ считывается один раз, что приводит к списку, затем ключ извлекается, который снова возвращает тот же список, и удаляет ключ.

Aaron 10.05.2024 19:02

Метод pop вернет список, связанный с old_key, который будет передан методу расширения. Метод расширения снова установит old_key; поскольку к этому моменту он не будет существовать, он инициализирует его пустым списком и расширит этот пустой список списком, полученным из метода pop.

Код выполняется не так. Python обычно выполняется строго слева направо, поэтому порядок вычислений следующий:

  • загрузить dict1
  • получить товар 'old_key'
  • получить атрибут extend
  • загрузить dict1
  • получить атрибут pop
  • позвони pop
  • позвони extend

Итак, extend работает с dict['old_key'], который впоследствии был извлечен из словаря, создавая циклический список, который затем сбрасывается на землю. По сути, ваш код делает это:

f = dict1['old_key'].extend
f(dict1.pop('old_key'))

Теперь вы понимаете, почему это не работает?

Вы можете подтвердить это, декомпилировав код через dis:

>>> dis.dis("dict1['old_key'].extend(dict1.pop('old_key'))")
  0           0 RESUME                   0

  1           2 LOAD_NAME                0 (dict1)
              4 LOAD_CONST               0 ('old_key')
              6 BINARY_SUBSCR
             10 LOAD_ATTR                3 (NULL|self + extend)
             30 LOAD_NAME                0 (dict1)
             32 LOAD_ATTR                5 (NULL|self + pop)
             52 LOAD_CONST               0 ('old_key')
             54 CALL                     1
             62 CALL                     1
             70 RETURN_VALUE

В вашей последовательности pop вызывается перед расширением. А для расширения требуется результат pop, потому что он передается как параметр. Таким образом, значение ключа должно быть установлено. Я не понимаю вывода dis и никогда им не пользовался.

AllSolutions 10.05.2024 18:45

Можете ли вы объяснить, что вы подразумеваете под: «Итак, расширение выполняется на dict['old_key'], который впоследствии был извлечен из dict, создавая циклический список, который затем сбрасывается на землю». Что сброшено и на каком основании?

AllSolutions 10.05.2024 18:47

К тому времени, как dict["old_key"] будет установлен с новым значением, если ключ больше не существует в dict из-за всплывающего окна, не должно ли это привести к исключению?

AllSolutions 10.05.2024 18:51
dict1['old_key'] преобразуется в список, который не присвоен ни одной переменной. Затем вы вставляете ключ, который удаляет ключ из словаря и возвращает другой экземпляр того же списка. затем вы вызываете метод расширения для первого экземпляра списка и передаете его второму экземпляру списка. Однако ключ dict был извлечен, и у вас не осталось ссылки на список, поэтому он будет удален.
Aaron 10.05.2024 18:57

Отличное объяснение! Я думаю, ваш пример мог бы быть более понятным, если бы разбивка выглядела примерно так: tmp = dict1['old_key']; tmp.extend(dict1.pop('old_key'))

Woodford 10.05.2024 19:25

@Аарон, операция dict1['old_key'].extend не завершается до тех пор, пока значение ключа не будет установлено и переназначено, поэтому, если к этому моменту ключ будет извлечен, должно было быть выдано исключение

AllSolutions 10.05.2024 19:47

@Аарон Хорошо, если подумать, я думаю, что твое объяснение имеет смысл, потому что операция расширения действительно завершена. Просто у нас теперь нет возможности ссылаться на расширенный список. Можете ли вы опубликовать это как ответ, чтобы я мог принять его?

AllSolutions 10.05.2024 19:53

@AllSolutions нигде нет «настройки и переназначения», extend обновит список на месте. А поскольку pop выполняется между получением списка и его расширением, расширяемый список удаляется из словаря, поэтому словарь оказывается пустым. dict1['old_key'].extend это не одна операция, а четыре (включая сам звонок). Перечисленные мной шаги, хотя и несколько упрощены, показывают, как Python на самом деле выполняет код, именно в таком порядке. То есть параметр функции оценивается между получением указателя на функцию и ее вызовом.

Masklinn 10.05.2024 20:40

Объяснение @Aaron прояснило это, и я проверил это, написав пример кода, чтобы продемонстрировать, что операция расширения действительно завершена, как ожидалось, но на расширенный список не ссылается ни один ключ в словаре.

Чтобы проверить это, я написал собственный класс и функцию, которая не только расширяет список, но и возвращает расширенный список, а затем я использую присваивание в той же строке, чтобы переназначить расширенный список той же клавише. Я не смог бы сделать это с помощью метода list.extend без написания собственного класса, поскольку метод расширения возвращает None.

class MyList(list):
    def extend_and_return(self, iterable):
        self.extend(iterable)
        return self

def my_list_factory():
    return MyList()

dict2 = defaultdict(my_list_factory)

dict2['old_key'] = MyList([{'name': 'A'}])
dict2['old_key'] = dict2['old_key'].extend_and_return(dict2.pop('old_key'))
print(dict2)
Ответ принят как подходящий

TLDR; Я думаю, что это самый простой способ объяснить, что происходит:

temp1 = dict1["old_key"]
temp2 = dict1.pop("old_key")
temp1.extend(temp2)
print(dict1)

Просматривая байт-код и учитывая, что сейчас находится в стеке, можно расшифровать выходные данные dis.dis.

1: dict1["old_key"] сначала упрощается до значения, на которое он указывает, то есть изначально созданного списка:

2 LOAD_NAME                0 (dict1)
4 LOAD_CONST               0 ('old_key')
6 BINARY_SUBSCR

Стек интерпретатора на этом этапе будет выглядеть примерно так:

<builtins.list object at 0x1234>

2: Затем ищется атрибут extend:

10 LOAD_ATTR                3 (NULL|self + extend)

куча:

<built-in method extend of list object at 0x1234>

3: Загрузите диктовку и получите атрибут pop. а также загрузите значение «old_key».

30 LOAD_NAME                0 (dict1)
32 LOAD_ATTR                5 (NULL|self + pop)
52 LOAD_CONST               0 ('old_key')
<built-in method extend of list object at 0x1234>
<built-in method pop of collections.defaultdict object at 0x4321>
"old_key"

4: всплывающий вызов («old_key» удаляется из <dict at 4321> и возвращается <list at 1234>)

54 CALL                     1
<built-in method extend of list object at 0x1234>
<builtins.list object at 0x1234>

5: вызов расширения (<list at 1234> модифицируется на месте методом расширения (возвратное значение не помещается в стек))

62 CALL                     1

Другие вопросы по теме