Некоторое время назад мой друг спросил меня, есть ли в Python структура, похожая на словарь, которая при неудачном поиске ключа вставляла бы ключ с самим собой в качестве значения, так что вы получали бы d[k] = k.

defaultdict из модуля collections почти, но не совсем это. defaultdict точно такой же, как встроенный dict, за исключением того, что, если ключ не существует в словаре, он вызывает нулевую (математическую основную функцию для 0-аргумента), называемую фабрикой по умолчанию, для установки значения для этого ключа. . Например, вы можете использовать конструкторы list, dict, set или int для получения пустой коллекции или 0, если ключ отсутствует.

Модель данных Python определяет специальный метод __missing__, который вызывается при сбое поиска ключа в dict. Мы можем очень легко переопределить defaultdict.__missing__, чтобы он вызывал фабрику по умолчанию с ключом… но это не так интересно, как изменение исходного кода CPython, чтобы сделать то же самое.

Из разговоров со своим знакомым James Powell я знал, что модуль collections написан на C, а не на Python. Глядя на Руководство разработчика Python, особенно на введение, вы видите, что большая часть кода C находится в каталоге стандартной библиотеки Modules. Исходный код модуля collections находится в Modules/_collections.c. Реализация defaultdict начинается со строки 1926 в Python 3.7; найдите в файле тип defaultdict, чтобы найти его в других версиях.

defaultdict реализован примерно в 300 строках кода C (не считая реализации dict, от которой defaultdict, очевидно, отталкивается). Большая часть этого кода не имеет отношения к тому, что мы хотим сделать, и большая его часть представляет собой строки документации или шаблоны, используемые для создания интерфейса Python для defaultdict.

Вот определение типа.

/* defaultdict type *********************************************************/
typedef struct {
 PyDictObject dict;
  PyObject *default_factory;
} defdictobject;

Поскольку это C, вы не можете наследовать от другого класса, но эта структура очень лаконично объясняет, что такое defaultdict: у вас есть словарь плюс указатель на фабрику по умолчанию. Так как defaultdict реализован на C, мы можем найти реализацию __missing__ немного ниже:

static PyObject *
defdict_missing(defdictobject *dd, PyObject *key)
{
  PyObject *factory = dd->default_factory;
  PyObject *value;
  if (factory == NULL || factory == Py_None) {
    /* XXX Call dict.__missing__(key) */
    PyObject *tup;
    tup = PyTuple_Pack(1, key);
    if (!tup) return NULL;
    PyErr_SetObject(PyExc_KeyError, tup);
    Py_DECREF(tup);
    return NULL;
  }
  value = PyEval_CallObject(factory, NULL);
  if (value == NULL)
    return value;
  if (PyObject_SetItem((PyObject *)dd, key, value) < 0) {
    Py_DECREF(value);
    return NULL;
  }
  return value;
}

Ключевым элементом здесь является PyEval_CallObject, который определен в Include/ceval.h. На самом деле это макрос для PyEval_CallObjectWithKeywords, но с аргументами ключевого слова, установленными на NULL. PyEval_CallObjectWithKeywords определяется в Objects/call.c:

/* External interface to call any callable object.
The args must be a tuple or NULL. The kwargs must be a dict or NULL. */
PyObject *
PyEval_CallObjectWithKeywords(PyObject *callable, PyObject *args, PyObject *kwargs)
{
  #ifdef Py_DEBUG
  /* PyEval_CallObjectWithKeywords() must not be called with an exception
set. It raises a new exception if parameters are invalid or if
PyTuple_New() fails, and so the original exception is lost. */
  assert(!PyErr_Occurred());
  #endif
  if (args != NULL && !PyTuple_Check(args)) {
    PyErr_SetString(PyExc_TypeError, “argument list must be a tuple”);
    return NULL;
  }
  if (kwargs != NULL && !PyDict_Check(kwargs)) {
    PyErr_SetString(PyExc_TypeError, “keyword list must be a dictionary”);
    return NULL;
  }
  if (args == NULL) {
    return _PyObject_FastCallDict(callable, NULL, 0, kwargs);
  }
  else {
    return PyObject_Call(callable, args, kwargs);
  }
}

Очень интересно. Первый аргумент — это ссылка на callable, затем идут *args и **kwargs. На что следует обратить внимание позже: как CPython обрабатывает *args отдельно от стандартного кортежа аргументов.

Я думаю, мы будем в порядке с созданием 1-кортежа, содержащего ключ, а затем передать его в PyEval_CallObject. Вот так:

static PyObject *
defdict_missing(defdictobject *dd, PyObject *key)
{
  PyObject *factory = dd->default_factory;
  PyObject *factory_args = PyTuple_Pack(key);
  PyObject *value;
  if (factory == NULL || factory == Py_None) {
    /* XXX Call dict.__missing__(key) */
    PyObject *tup;
    tup = PyTuple_Pack(1, key);
    if (!tup) return NULL;
    PyErr_SetObject(PyExc_KeyError, tup);
    Py_DECREF(tup);
    return NULL;
  }
  value = PyEval_CallObject(factory, factory)args);
  if (value == NULL)
  return value;
  if (PyObject_SetItem((PyObject *)dd, key, value) < 0) {
    Py_DECREF(value);
    return NULL;
  }
  return value;
}

Посмотрим…

0:00:17 load avg: 2.12 [152/415/3] test_defaultdict failed
test test_defaultdict failed — multiple errors occurred; run in verbose mode for details

Думаю нет! test_defaultdict определяется в Lib/test/test_typing.py.

В следующий раз: мы проверим набор тестов и посмотрим, как работает новый defaultdict. Мы также обсудим, как я нашел свой путь в кодовой базе CPython.

Другие мысли:

1. Можем ли мы проверить, что default_factory не принимает никаких аргументов при создании defaultdict? Очевидно, мы можем. Более интересный вопрос, конечно, заключается в том, как. Я бы начал с проверки атрибута __code__.co_argcount фабрики по умолчанию.

2. Почему завод PyObject *?