6

I want to change the dir() output for my class. Normally, for all other objects, it's done by defining own __dir__ method in their class. But if I do this for my class, it's not called.

class X(object):
    def __dir__():
        raise Exception("No!")

>>>dir(X)
['__class__', '__delattr__', '__dict__',....

How can I change the dir() output for class?

martineau
  • 119,623
  • 25
  • 170
  • 301
George Shuklin
  • 6,952
  • 10
  • 39
  • 80
  • 1
    See [*Special method lookup*](https://docs.python.org/3/reference/datamodel.html#special-lookup); `type(X).__dir__` is used, not `X.__dir__`. – Martijn Pieters Sep 08 '17 at 16:05
  • "Normally, for all other objects, it's done by defining own `__dir__` method in their class" - same with this object. You define a `__dir__` method in this class's class, a.k.a. its metaclass. – user2357112 Sep 11 '17 at 23:27

2 Answers2

11

That's because dir calls the __dir__ of the type of the input (equivalent to: type(inp).__dir__(inp)). For instances of a class it will call the classes __dir__ but if called on a class it will call the __dir__ of the meta-class.

class X(object):
    def __dir__(self):  # missing self parameter
        raise Exception("No!")

dir(X())  # instance!
# Exception: No!

So if you want to customize dir for your class (not instances of your class) you need to add a metaclass for your X:

import six

class DirMeta(type):
    def __dir__(cls):
        raise Exception("No!")

@six.add_metaclass(DirMeta)
class X(object):
    pass

dir(X)
# Exception: No!
MSeifert
  • 145,886
  • 38
  • 333
  • 352
  • It worked, thanks. But now I stuck inside `__dir__`, because in reality I don't want to `raise` anything, I just want to remove few methods (from unittest's inspection). But I couldn't find any way to get access to `cls` content. I knew, I shouldn't go into this forest... – George Shuklin Sep 08 '17 at 16:27
  • Oh, I found `cls.__dict__.keys()`. Exactly what I need. – George Shuklin Sep 08 '17 at 16:39
  • 1
    @GeorgeShuklin The alternative would be to use the inheritance from `type`. For example using `normal_dir = super().__dir__()` (this is what would be normally shown when you call `dir`) and then modify (add/remove stuff) the `normal_dir` before returning it. But `cls.__dict__.keys()` should also work. :) – MSeifert Sep 09 '17 at 10:21
3

As @MSeifert already explained, dir calls the __dir__ attrbiute of the object's class. So type(X).__dir__ is called, not X.__dir__. Just for those that are interested, here is a look behinds the scenes of what exactly occurs.

The implementation of dir is in bltinmodule.c:

builtin_dir(PyObject *self, PyObject *args)
{
    PyObject *arg = NULL;

    if (!PyArg_UnpackTuple(args, "dir", 0, 1, &arg))
        return NULL;
    return PyObject_Dir(arg);
}

The dir function calls the API function PyObject_Dir. The PyObject_Dir function is implemented in object.c:

PyObject *
PyObject_Dir(PyObject *obj)
{
    return (obj == NULL) ? _dir_locals() : _dir_object(obj);
}

PyObject_Dir is defined using two helper functions. When an object is passed in - as is the case here - then the _dir_object function is called. It is also implemented in object.c:

static PyObject *
_dir_object(PyObject *obj)
{
    PyObject *result, *sorted;
    PyObject *dirfunc = _PyObject_LookupSpecial(obj, &PyId___dir__);

    assert(obj);
    if (dirfunc == NULL) {
        if (!PyErr_Occurred())
            PyErr_SetString(PyExc_TypeError, "object does not provide __dir__");
        return NULL;
    }
    /* use __dir__ */
    result = _PyObject_CallNoArg(dirfunc);
    Py_DECREF(dirfunc);
    if (result == NULL)
        return NULL;
    /* return sorted(result) */
    sorted = PySequence_List(result);
    Py_DECREF(result);
    if (sorted == NULL)
        return NULL;
    if (PyList_Sort(sorted)) {
        Py_DECREF(sorted);
        return NULL;
    }
    return sorted;
}

The part were focusing on is:

PyObject *dirfunc = _PyObject_LookupSpecial(obj, &PyId___dir__);

This is where the __dir__ special method is looked-up on the object passed in. This is done using _PyObject_LookupSpecial. _PyObject_LookupSpecial is defined in typeobject.c:

PyObject *
_PyObject_LookupSpecial(PyObject *self, _Py_Identifier *attrid)
{
    PyObject *res;

    res = _PyType_LookupId(Py_TYPE(self), attrid);
    if (res != NULL) {
        descrgetfunc f;
        if ((f = Py_TYPE(res)->tp_descr_get) == NULL)
            Py_INCREF(res);
        else
            res = f(res, self, (PyObject *)(Py_TYPE(self)));
    }
    return res;
}

_PyObject_LookupSpecial first calls Py_TYPE on the object passed in, before looking-up the attribute using _PyType_LookupId. Py_TYPE is a macro that gets the ob_type member of objects. It's expanded form is:

(((PyObject*)(o))->ob_type)

And as you probably guessed, The ob_type attribute is the class (or type) of the object:

typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;

So, as you can see from above, the __dir__ attribute used is indeed the object's class.

Christian Dean
  • 22,138
  • 7
  • 54
  • 87