Откуда Python знает, что кто-то зацикливался на дикте?

#python #list

Вопрос:

Если кто-нибудь попытается:

 my_dict = {1: 1}
for key in my_dict:
    my_dict.pop(key)
 

один получит:

 Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration
 

Python выдаст ошибку, так как вы изменили размер диктанта, просматривая его.

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

И прежде чем кто-нибудь задаст неизбежный вопрос «почему я хотел бы это сделать»: я этого не делаю. Я задаю вопрос. Это называется любопытством.

например:

Допустим, у меня есть диктант с 5 пунктами. Приведенный выше код должен просто удалить все элементы в диктанте!

Комментарии:

1. Вы имеете в виду добавить a 5 для каждого из элементов my_list , присутствующих перед началом цикла?

2. Нет, я хочу сделать это во время цикла. И да, я могу просто перебирать длину списка, а не его элементы, но я конкретно спрашиваю, могу ли я переопределить Python, чтобы не выдавать здесь ошибку.

3. Python в этом случае НЕ выдает ошибку. Это просто создает бесконечный цикл. Вы пробовали это сделать?

4. Первоначально словари Python не имели гарантированного порядка. Они повторялись в любом порядке, в каком бы ни находилась древовидная структура. Таким образом, добавление элемента может привести к перебалансировке дерева и искажению порядка, что сделает итератор бесполезным. Он больше не будет знать, где находится.

5. Вот соответствующий источник CPython: github.com/python/cpython/blob/…

Ответ №1:

Если вы поищете в исходном коде Python «словарь изменился в размере во время итерации», вы найдете Objects/dictobject.c :

 static PyObject*
dictiter_iternextkey(dictiterobject *di)
{
    /* ... omitted ... */

    if (di->di_used != d->ma_used) {
        PyErr_SetString(PyExc_RuntimeError,
                        "dictionary changed size during iteration");
        di->di_used = -1; /* Make this state sticky */
        return NULL;
    }
 

ma_used Поле-это просто количество элементов в словаре, как описано в dictobject.h :

 /* Number of items in the dictionary */
Py_ssize_t ma_used;
 

И di_used является просто копией этого значения с момента создания итератора.

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

Причина, по которой Python делает это, заключается в том, что трудно понять, что «правильно» делать, когда вы повторяете изменяющуюся хэш-таблицу.

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

Комментарии:

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

2. @wim: Даже если результаты могут быть четко определены, реализация может быть неоправданно сложной.

Ответ №2:

Объект Python может возвращать свои собственные итераторы. Видишь https://wiki.python.org/moin/Iterator. Поэтому, когда __iter__() вызывается объект dict, dict может установить флаг, по которому он повторяется. Этот же флаг будет снят, как только внутренний итератор израсходует все элементы в dict. Если в dict вызываются какие-либо изменения (например, с помощью pop ), функция проверяет флаг, чтобы узнать, можно ли внести изменения или если dict все еще находится в цикле итератора.