Некоторое время назад мой друг спросил меня, есть ли в 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 *?