Specially document the "__getattr__" behavior for "object.__getattr__"

This is a strange behavior for static type:

#include <Python.h>

typedef struct {
    PyObject_HEAD
    PyObject* proxy;
} ProxyObject;

static PyObject* ProxyObject_getattr(ProxyObject* self, PyObject* key) {
    if (!self->proxy) {
        PyErr_SetString(PyExc_ValueError, "no proxy");
        return NULL;
    }
    return PyObject_GetAttr(self->proxy, key);
}

static PyMethodDef Proxy_methods[] = {
    {"__getattr__", (PyCFunction)ProxyObject_getattr, METH_O, 
     "Get attribute from proxied object"},
    {NULL}  /* Sentinel */
};

static int ProxyObject_init(ProxyObject* self, PyObject* args, PyObject* kwds) {
    PyObject* obj;
    if (!PyArg_ParseTuple(args, "O", &obj)) {
        return -1;
    }
    self->proxy = obj;
    return 0;
}

static PyTypeObject ProxyType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "proxy.Proxy",
    .tp_basicsize = sizeof(ProxyObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_methods = Proxy_methods,
    .tp_new = PyType_GenericNew,
    .tp_init = (initproc)ProxyObject_init,
};

static struct PyModuleDef proxymodule = {
    PyModuleDef_HEAD_INIT,
    "proxy",
    "Proxy module",
    -1,
    NULL, NULL, NULL, NULL, NULL
};

PyMODINIT_FUNC PyInit_proxy(void) {
    PyObject* m;
    if (PyType_Ready(&ProxyType) < 0)
        return NULL;

    m = PyModule_Create(&proxymodule);
    if (m == NULL)
        return NULL;

    Py_INCREF(&ProxyType);
    PyModule_AddObject(m, "Proxy", (PyObject*)&ProxyType);
    return m;
}

Then we can test:

>>> import proxy
>>> x = proxy.Proxy(1)
>>> x.real
Traceback (most recent call last):
  File "<pyshell#0>", line 1, in <module>
    x.real
AttributeError: 'proxy.Proxy' object has no attribute 'real'
>>> x.__getattr__("real")
1
>>> proxy.Proxy.__getattribute__ is object.__getattribute__
True
>>> proxy.Proxy.__getattr__
<method '__getattr__' of 'proxy.Proxy' objects>

As you see: the “__getattr__” didn’t be called when “__getattribute__” raise AttributeError (we didn’t change “__getattribute__”).

Here is the document:

object.__getattr__(self, name )

Called when the default attribute access fails with an AttributeError (either __getattribute__() raises an AttributeError because name is not an instance attribute or an attribute in the class tree for self; or __get__() of a name property raises AttributeError). This method should either return the (computed) attribute value or raise an AttributeError exception. The object class itself does not provide this method.

Note that if the attribute is found through the normal mechanism, __getattr__() is not called. (This is an intentional asymmetry between __getattr__() and __setattr__().) This is done both for efficiency reasons and because otherwise __getattr__() would have no way to access other attributes of the instance. Note that at least for instance variables, you can take total control by not inserting any values in the instance attribute dictionary (but instead inserting them in another object). See the __getattribute__() method below for a way to actually get total control over attribute access.

This might be an explaination for it:

If the type is a static type, its tp_getattro defaults to just find attribute on type and won’t respect “__getattr__”. So we need to document that when write c extern module, don’t use “__getattr__” to control when attribute not found.

Test on 3.14

This is not how CPython’s C-API works.

You were reading The Python Language Reference, which accurately describes the Python Language:

>>> class Proxy:
...     def __init__(self, key):
...         self.proxy = key
...     def __getattr__(self, key):
...         return getattr(self.proxy, key)
...         
>>> 
>>> x = Proxy(1)
>>> x.real
1

In the C API, this works differently and is explained in the Python/C API Reference Manual. You have used the tp_methods slot, which is as mechanism to provide “regular methods”. In order to provide functionality in C that corresponds to a dunder method in Python, the appropriate type slot, like tp_getattro has to be used.

1 Like

I know. However, I think that “some dunder methods will not work” is needed to be documented. One counterexample is “__dir__”. For some dunder names, I think that it is needed to note that in C-API it won’t work.

This does not belong to the Python language specs.

The docs for tp_methods should be more clear on what a “regular method” is: a method for which a specific type slot does not exist. Since there are no type slots for __dir__, or __copy__ or __my_fancy_new_dunder_function__, such methods would be considered regular methods.

1 Like

This should be type(x).__getattr__("real") to be correct.

The currect is type(x).__getattr__(x, "real"). In fact it can work but it is ignored when getattr. When the users first try to just “get the attribute from the other place” in capi, they may make this mistake that define “__getattr__” in “tp_method” and then feel strange why it doesn’t work. This is what the document didn’t explicitly state.