rorycl
rorycl

Reputation: 1384

Is it possible to overload a call to vars(object) in Python?

I have a class that overloads object attribute access by returning the attributes of its "row" attribute, along the following lines:

from collections import namedtuple
class MyObj(object):
    def __init__(self, y, z):
        r = namedtuple('row', 'a b')
        self.row = r(y, z)
        self.arbitrary = True
    def __getattr__(self, attr):
        return getattr(self.row, attr)
    def __dir__(self):
        return list(self.row._fields)

In [2]: m = MyObj(1, 2)
In [3]: dir(m)
Out[3]: ['a', 'b']
In [4]: m.a
Out[4]: 1
In [5]: vars(m)
Out[5]: {'arbitrary': True, 'row': row(a=1, b=2)}
In [6]: output = '{a} -> {b}'
In [7]: output.format(**vars(m.row))
Out[7]: '1 -> 2'
In [8]: output.format(**vars(m))
KeyError: 'a'

As I quite often do string formatting using vars() I'd like to be able to access row's attributes directly from the call to vars(). Is this possible?


Edit following aaronsterling's answer

The key to solving this, thanks to Aaron's pointer, is to check for __dict__ in __getattribute__

from collections import namedtuple
class MyObj(object):
    def __init__(self, y, z):
        r = namedtuple('row', 'a b')
        self.row = r(y, z)
        self.arbitrary = True
    def __getattr__(self, attr):
        return getattr(self.row, attr)
    def __getattribute__(self, attribute):
        if attribute == '__dict__':
            return self.row._as_dict()
        else:
            return object.__getattribute__(self, attribute)
    def __dir__(self):
        return list(self.row._fields)

In [75]: m = MyObj(3, 4)

In [76]: m.a
Out[76]: 3

In [77]: vars(m)
Out[77]: OrderedDict([('a', 3), ('b', 4)])

Upvotes: 5

Views: 2430

Answers (2)

nicholishen
nicholishen

Reputation: 3012

You can also define a @property for __dict__ instead of overriding __getattribute__.

class Example:
    @property
    def __dict__(self):
        return "From vars()"

e = Example()
print(vars(e)) # From vars()

Upvotes: 4

aaronasterling
aaronasterling

Reputation: 71054

The docs are so kind as to specify that vars works by returning the __dict__ attribute of the object it's called on. Hence overriding __getattribute__ does the trick. Y

I subclass dict (but see later) to override the __str__ function. The subclass accepts a function str_func which gets called to return a string representation of how you want your __dict__ object to appear. It does this by constructing a regular dictionary with the entries that you want and then calling str on that.

This is very hacky. In particular, it will break any code that depends on doing anything like

myobj.__dict__[foo] = bar

This code will now update a phantom dictionary and not the real one.

A much more robust solution would depend on completely replacing all methods that set values on the SpoofedDict with methods that actually update myobj.__dict__. This would require SpoofedDict instances to hold a reference to myobj.__dict__. Then of course, the methods that read values would have to fetch them out of myobj.__dict__ as well.

At that point, you're better off using collections.Mapping to construct a custom class rather than subclassing from dict.

Here's the proof of concept code, hackish as it may be:

from collections import namedtuple


class SpoofDict(dict):
    def __init__(self, *args, **kwargs):
        self.str_func = kwargs['str_func']
        del kwargs['str_func']

        dict.__init__(self, *args, **kwargs)

    def __str__(self):
        return self.str_func()


class MyObj(object):
    def __init__(self, y, z):
        r = namedtuple('row', 'a b')
        self.row = r(y, z)
        self.arbitrary = True
    def __getattr__(self, attr):
        return getattr(self.row, attr)

    def __dir__(self):
        return list(self.row._fields)

    def str_func(self):
        attrs = list(self.row._fields)
        str_dict = {}
        row = object.__getattribute__(self, 'row')
        for attr in attrs:
            str_dict[attr] = getattr(row, attr)
        return str(str_dict)

    def __getattribute__(self, attribute):
        if attribute == '__dict__':
            spoof_dict = SpoofDict(str_func=self.str_func)
            spoof_dict.update(object.__getattribute__(self, '__dict__'))
            return spoof_dict                
        else:
            return object.__getattribute__(self, attribute)


if __name__=='__main__':
    m = MyObj(1, 2)
    print "dir(m) = {0}".format(dir(m))
    print "vars(m) = {0}".format(vars(m))
    print "m.row = {0}".format(m.row)
    print "m.arbitrary = {0}".format(m.arbitrary)

Upvotes: 2

Related Questions