Что именно происходит в бесконечных вложенных списках?

Возможно создание бесконечного вложенного списка в Python. Это ясно и, хотя и не популярно и определенно не полезно, является известным фактом.

>>> a = [0]
>>> a[0] = a
>>> a
[[...]]
>>> a[0] == a
True

Мой вопрос: что здесь происходит:

>>> a = [0]
>>> b = [0]
>>> a[0], b[0] = b, a
>>> a
[[[...]]]
>>> b
[[[...]]]
>>> a == b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: maximum recursion depth exceeded in cmp
>>> a[0] == b
True
>>> a[0][0] == b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: maximum recursion depth exceeded in cmp
>>> a[0][0][0] == b
True
>>> 

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

Теперь мои вопросы об этом. У нас действительно есть два списка, или только один? Как такая вещь хранится в памяти? Что может быть целью, позволяющей программистам реализовать что-то настолько странное, как это?

Пожалуйста, не рассматривайте этот вопрос супер-серьезным. И не забывайте, что программирование иногда может быть забавным.

Ответ 1

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

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

Когда я пишу b[0] = a, это не означает, что a находится в b. Это означает, что b содержит ссылку, указывающую на то, что указывает a.

Переменные a и b сами по себе сами не являются "вещами", и сами они также являются просто указателями на анонимные "вещи" в памяти.

Концепция ссылок - это серьезный скачок от мира, не связанного с программированием, поэтому давайте рассмотрим это в вашей программе:

>>> a = [0]

Вы создаете список, который имеет что-то в нем (на этот раз игнорируйте это). Важно то, что это список. Этот список сохраняется в памяти. Пусть говорят, что он хранится в ячейке памяти 1001. Затем присваивание = создает переменную a, которую язык программирования позволяет вам использовать позже. На данный момент есть некоторый объект списка в памяти и ссылка на него, с которой вы можете получить доступ с именем a.

>>> b = [0]

Это делает то же самое для b. Существует новый список, который хранится в ячейке памяти 1002. Язык программирования создает ссылку b, которую вы можете использовать для ссылки на ячейку памяти и, в свою очередь, объект списка.

>>> a[0], b[0] = b, a

Это делает две вещи одинаковыми, поэтому давайте сосредоточимся на одном: a[0] = b. То, что это делает, очень причудливо. Сначала он оценивает правую часть равенства, видит переменную b и выбирает соответствующий объект в памяти (объект памяти # 1002), так как b является ссылкой на него. То, что происходит с левой стороны, одинаково причудливо. a - это переменная, которая указывает на список (объект памяти # 1001), но сам объект памяти # 1001 имеет несколько собственных ссылок. Вместо тех ссылок, которые имеют имена типа a и b, которые вы используете, эти ссылки имеют числовые индексы, такие как 0. Итак, теперь это то, что это a вызывает объект памяти # 1001, который является кучей индексированных ссылок, и он переходит к ссылке с индексом 0 (ранее эта ссылка указывала на фактическое число 0, которое это то, что вы делали в строке 1), а затем переустанавливает эту ссылку (т.е. первую и единственную ссылку в объекте памяти # 1001) на то, что оценивает вещь в правой части уравнения. Итак, теперь объект # 1001 0-я ссылка указывает на объект # 1002.

>>> a
[[[...]]]
>>> b
[[[...]]]

Это просто фантазия, сделанная языком программирования. Когда вы просто попросите его оценить a, он вытащит объект памяти (список в местоположении # 1001), обнаруживает, используя свою собственную магию, что он бесконечен и отображает себя как таковой.

>>> a == b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: maximum recursion depth exceeded in cmp

Ошибка этого оператора связана с тем, как Python выполняет сравнения. Когда вы сравниваете объект с самим собой, он сразу же оценивает значение true. Когда вы сравниваете и возражаете против другого объекта, он использует "магию", чтобы определить, должно ли равенство быть истинным или ложным. В случае списков в Python он просматривает каждый элемент в каждом списке и проверяет, равны ли они (в свою очередь, используя собственные методы проверки элементов). Итак, при попытке a == b. То, что он делает, сначала выкапывает b (объект # 1002) и (объект # 1001), а затем понимает, что они разные вещи в памяти, поэтому переходит к его рекурсивной проверке списка. Он делает это, итерируя через два списка. Объект # 1001 имеет один элемент с индексом 0, который указывает на объект # 1002. Объект # 1002 имеет один элемент с индексом 0, который указывает на объект # 1001. Таким образом, программа делает вывод о том, что объекты # 1001 и # 1002 равны, если все их ссылки указывают на одно и то же, ergo, если # 1002 (что означает только 1001 опорные точки) и # 1001 (что означает только опорные точки # 1002) тоже самое. Эта проверка равенства никогда не прекратится. То же самое произойдет в любом списке, который не остановится. Вы могли бы сделать c = [0]; d = [0]; c[0] = d; d[0] = c и a == c, чтобы вызвать ту же ошибку.

>>> a[0] == b
True

Как я уже указывал в предыдущем абзаце, это немедленно разрешает true, потому что Python принимает ярлык. Не нужно сравнивать содержимое списка, потому что a[0] указывает на объект # 1002 и b указывает на объект # 1002. Python обнаруживает, что они идентичны в буквальном смысле (они - одна и та же вещь) и даже не утруждают себя проверкой содержимого.

>>> a[0][0] == b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: maximum recursion depth exceeded in cmp

Это возвращается к ошибке, потому что a[0][0] заканчивается, указывая на объект # 1001. Проверка удостоверения не выполняется и возвращается к рекурсивной проверке содержимого, которая никогда не заканчивается.

>>> a[0][0][0] == b
True

Еще раз, a[0][0][0] указывает на объект # 1002, как и на b. Рекурсивная проверка пропускается, и сравнение немедленно возвращает true.


jabber jibber более высокого уровня напрямую не связан с вашим конкретным фрагментом кода:

  • Так как все есть ссылки, относящиеся к другим объектам, хотя существует то, что кажется "бесконечным" вложением, объект, на который ссылается a (как я назвал объект # 1001), и объект, упомянутый be b (# 1002) имеют одинаковый размер в памяти. И этот размер на самом деле невероятно мал, поскольку все они являются списками, указывающими на соответствующие другие ячейки памяти.
  • Также стоит отметить, что на менее "щедрых" языках сравнение двух ссылок с == возвращает true только, если объекты памяти, на которые они указывают, одинаковы в том смысле, что обе ссылки указывают на одно и то же место в памяти. Java - пример этого. Стилистическое соглашение, появившееся на таких языках, заключается в том, чтобы определить метод/функцию для самих объектов (для Java, это условно называется equals()), чтобы выполнить выборочное тестирование равенства. Python делает это из списка для списков. Я не знаю, в частности, о Python, но, по крайней мере, в Ruby, == перегружен в том смысле, что когда вы делаете someobject == otherobject, он на самом деле вызывает метод под названием == на someobject (который вы можете перезаписать). Теоретически, вам нечего было бы заставить someobject == otherobject возвращать нечто, отличное от логического.

Ответ 2

Я подозреваю, что следующее:

a[0]==b: Python просматривает значение a[0] и находит какую-то ссылку на b, поэтому он говорит True.

a[0][0]==b: Python смотрит вверх a[0], находит b и теперь смотрит вверх a[0][0], который есть (поскольку a[0] содержит b) b[0]. Теперь он видит, что b[0] содержит некоторую ссылку на a, что не совсем то же самое, что b. Таким образом, python должен сравнивать элементы, что означает, что он должен сравнивать a[0] с b[0]. Теперь начинается бесконечная рекурсия...

Обратите внимание, что это работает только потому, что Python фактически не копирует список при назначении a[0]=b. Python скорее создает ссылку на b, которая хранится в a[0].

Ответ 3

a[0] относится к b, а b[0] относится к a. Это круговая ссылка. Как упоминал glglgl, при попытке оператора == он пытается сравнить значения.

Попробуйте это, что может сделать вещи более ясными -

>>> id(a)
4299818696
>>> id(b)
4299818768
>>> id(a[0])
4299818768
>>> 
>>> id(b[0])
4299818696

Ответ 4

Я вижу, что a содержит b, содержащий a

Они не содержат друг друга как таковые - A является ссылкой на список, первое, что в этом списке является ссылкой на B, и наоборот

>>> a[0] == b
True
>>> a[0][0] == b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: maximum recursion depth exceeded in cmp
>>> a[0][0][0] == b
True

Здесь число [0] не имеет значения, так как вы можете делать столько запросов списка, сколько захотите, - важно то, что в примерах # 1 и # 3 (и все нечетные числа поисков) вы говорите "B равно B", в этот момент python сравнивает адреса памяти и видит, что они одно и то же, так что да. В примере # 2 (и всех поисковых запросах) вы говорите: "A равно B", python видит, что они разные адреса памяти, а затем пытается загрузить всю (бесконечную) структуру данных в память, глубинное сравнение.

Ответ 5

Это два списка. Сначала создайте их:

a = [0]
b = [0]

И затем вы назначаете каждый из них первому элементу другого:

a[0], b[0] = b, a

Итак, вы можете сказать

a[0] is b

и

b[0] is a

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

Кроме того, вы не сравниваете идентичность (is), а для равенства (==). Это приводит к попытке сравнить их - глубоко внутри, что приводит к рекурсии.