Почему этот простой код 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 строки: сначала вытолкнуть и сохранить результат в значении, а затем расширить.
Или, прежде чем устанавливать новый ключ, я могу проверить, находится ли новый ключ уже в словаре или такой же, как и извлекаемый ключ.
Но я хочу понять, что вызывает странное поведение кода.






выходные данные будут пустым словарем, поскольку список, связанный с ключом «old_key», был перемещен в новый ключ «old_key», а затем исходный ключ «old_key» удален. Следовательно, после этих операций словарь dict1 будет пустым.
Это не совсем правильно, поскольку список не «перемещается на новый ключ». Ключ считывается один раз, что приводит к списку, затем ключ извлекается, который снова возвращает тот же список, и удаляет ключ.
Ваш ответ можно улучшить, добавив дополнительную вспомогательную информацию. Пожалуйста, отредактируйте , добавив дополнительную информацию, например цитаты или документацию, чтобы другие могли подтвердить правильность вашего ответа. Более подробную информацию о том, как писать хорошие ответы, вы можете найти в справочном центре.
Метод pop вернет список, связанный с old_key, который будет передан методу расширения. Метод расширения снова установит old_key; поскольку к этому моменту он не будет существовать, он инициализирует его пустым списком и расширит этот пустой список списком, полученным из метода pop.
Код выполняется не так. Python обычно выполняется строго слева направо, поэтому порядок вычислений следующий:
dict1'old_key'extenddict1poppopextendИтак, 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 и никогда им не пользовался.
Можете ли вы объяснить, что вы подразумеваете под: «Итак, расширение выполняется на dict['old_key'], который впоследствии был извлечен из dict, создавая циклический список, который затем сбрасывается на землю». Что сброшено и на каком основании?
К тому времени, как dict["old_key"] будет установлен с новым значением, если ключ больше не существует в dict из-за всплывающего окна, не должно ли это привести к исключению?
dict1['old_key'] преобразуется в список, который не присвоен ни одной переменной. Затем вы вставляете ключ, который удаляет ключ из словаря и возвращает другой экземпляр того же списка. затем вы вызываете метод расширения для первого экземпляра списка и передаете его второму экземпляру списка. Однако ключ dict был извлечен, и у вас не осталось ссылки на список, поэтому он будет удален.
Отличное объяснение! Я думаю, ваш пример мог бы быть более понятным, если бы разбивка выглядела примерно так: tmp = dict1['old_key']; tmp.extend(dict1.pop('old_key'))
@Аарон, операция dict1['old_key'].extend не завершается до тех пор, пока значение ключа не будет установлено и переназначено, поэтому, если к этому моменту ключ будет извлечен, должно было быть выдано исключение
@Аарон Хорошо, если подумать, я думаю, что твое объяснение имеет смысл, потому что операция расширения действительно завершена. Просто у нас теперь нет возможности ссылаться на расширенный список. Можете ли вы опубликовать это как ответ, чтобы я мог принять его?
@AllSolutions нигде нет «настройки и переназначения», extend обновит список на месте. А поскольку pop выполняется между получением списка и его расширением, расширяемый список удаляется из словаря, поэтому словарь оказывается пустым. dict1['old_key'].extend это не одна операция, а четыре (включая сам звонок). Перечисленные мной шаги, хотя и несколько упрощены, показывают, как Python на самом деле выполняет код, именно в таком порядке. То есть параметр функции оценивается между получением указателя на функцию и ее вызовом.
Объяснение @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
Возможный дубликат stackoverflow.com/questions/6777485/…