If you use a custom dict subclass for a function’s globals, you can override __getitem__
and this will be respected because the interpreter when processing LOAD_GLOBAL
calls PyMapping_GetOptionalItem
which respects user overrides. But STORE_GLOBAL
results in a call to PyDict_SetItem
, and that call assumes the exact dictionary type and ignores user overrides. It seems weird for it to not be symmetrical.
The LOAD_GLOBAL
code (see below) has a fast path for when it’s really a regular dictionary. Any reason to not also do that in the STORE_GLOBAL
case?
Test case demonstrating behavior:
import types
class MyGlobals(dict):
def __getitem__(self, key):
print(f"__getitem__ called for key: {key}")
return super().__getitem__(key)
def __setitem__(self, key, value):
print(f"__setitem__ called for key: {key} with value: {value}")
super().__setitem__(key, value)
def func():
global x, y
x = 10
y = x + 5
print(f"x = {x}, y = {y}")
custom_globals = MyGlobals()
custom_globals.update(func.__globals__)
func = types.FunctionType(
func.__code__,
custom_globals,
name=func.__name__,
argdefs=func.__defaults__,
closure=func.__closure__
)
func()
print(f"Value of 'x' in custom globals: {custom_globals.get('x')}")
print(f"Value of 'y' in custom globals: {custom_globals.get('y')}")
Which gives this output (notice the __setitem__
print is missing):
__getitem__ called for key: x
__getitem__ called for key: print
__getitem__ called for key: x
__getitem__ called for key: y
x = 10, y = 15
Value of 'x' in custom globals: 10
Value of 'y' in custom globals: 15
Code for loading globals:
void
_PyEval_LoadGlobalStackRef(PyObject *globals, PyObject *builtins, PyObject *name, _PyStackRef *writeto)
{
if (PyDict_CheckExact(globals) && PyDict_CheckExact(builtins)) {
_PyDict_LoadGlobalStackRef((PyDictObject *)globals,
(PyDictObject *)builtins,
name, writeto);
if (PyStackRef_IsNull(*writeto) && !PyErr_Occurred()) {
/* _PyDict_LoadGlobal() returns NULL without raising
* an exception if the key doesn't exist */
_PyEval_FormatExcCheckArg(PyThreadState_GET(), PyExc_NameError,
NAME_ERROR_MSG, name);
}
}
else {
/* Slow-path if globals or builtins is not a dict */
/* namespace 1: globals */
PyObject *res;
if (PyMapping_GetOptionalItem(globals, name, &res) < 0) {
*writeto = PyStackRef_NULL;
return;
}
if (res == NULL) {
/* namespace 2: builtins */
if (PyMapping_GetOptionalItem(builtins, name, &res) < 0) {
*writeto = PyStackRef_NULL;
return;
}
if (res == NULL) {
_PyEval_FormatExcCheckArg(
PyThreadState_GET(), PyExc_NameError,
NAME_ERROR_MSG, name);
}
}
*writeto = PyStackRef_FromPyObjectSteal(res);
}
}
Versus for storing:
TARGET(STORE_GLOBAL) {
frame->instr_ptr = next_instr;
next_instr += 1;
INSTRUCTION_STATS(STORE_GLOBAL);
_PyStackRef v;
v = stack_pointer[-1];
PyObject *name = GETITEM(FRAME_CO_NAMES, oparg);
_PyFrame_SetStackPointer(frame, stack_pointer);
int err = PyDict_SetItem(GLOBALS(), name, PyStackRef_AsPyObjectBorrow(v));
stack_pointer = _PyFrame_GetStackPointer(frame);
PyStackRef_CLOSE(v);
if (err) goto pop_1_error;
stack_pointer += -1;
assert(WITHIN_STACK_BOUNDS());
DISPATCH();
}