Search code examples
pythonpython-3.xoopmetaprogramming

how to make a derived class that logs all access to its members?


I'm trying to make a class that behaves like a dictionary, except any time one of its methods is called or one of its attributes is accessed the fact is logged. I'll clarify what I mean by showing the naive implementation I made (repetitive code is replaced with ellipsis):

class logdict(dict):
    def __init__(self, *args, **kwargs):
        self._log = [
            {'name': '__init__',
             'args': tuple(map(repr, args)),
             'kwargs': dict((key, repr(kwargs[key])) for key in kwargs)
             }
            ]
        return super().__init__(*args, **kwargs)
    def __getitem__(self, key):
        self._log.append({
            'name': '__getitem__',
            'args': (repr(key),),
            'kwargs': {}
            })
        return super().__getitem__(key)
    def __setitem__(self, key, value):
        ...
    def __delitem__(self, key):
        ...
    def __getattribute__(self, name):
        if name == '_log': #avoiding infinite recursion
            return super().__getattribute__(name)
        ...
    def __contains__(self, key):
        ...
    def logrepr(self):
        log = ''
        for logitem in self._log: #this is just formatting, nothing interesting here
            log += '{fun}({rargs}{optsep}{rkwargs})\n'.format(
                fun = logitem['name'],
                rargs = ', '.join(logitem['args']),
                optsep = ', ' if len(logitem['kwargs'])>0 else '',
                rkwargs = ', '.join('{} = {}'.format(key, logitem['kwargs'][key])
                                    for key in logitem['kwargs'])
                )
        return log

here, at least for the methods I overloaded, I'm saving which method is being called and the repr of its arguments (if I just saved the arguments, I run the risk of seeing the latest "version" of a mutable object instead of the old one). This implementation kinda works:

d = logdict()
d['1'] = 3
d['1'] += .5
print('1' in d)
print('log:')
print(d.logrepr())

produces:

True
log:
__init__()
__setitem__('1', 3)
__getitem__('1')
__setitem__('1', 3.5)
__contains__('1')
__getattribute__('logrepr')

however it's rather clunky and I'm never sure if I covered all the possible methods. Is there a more efficient way to do this, ideally that generalizes to any given class (and which wraps and logs all dunder methods, not only visible ones)?

Note: this is not a duplicate of this question, as the problem in it was how to avoid infinite recursion rather than how to automate/simplify the process of writing the derived class.


Solution

  • You could just auto-generate all methods of dict (with some exceptions), then you don't have to repeat yourself so much:

    from functools import wraps
    
    
    class LogDict(dict):
        logs = {}
    
        def _make_wrapper(name):
            @wraps(getattr(dict, name))
            def wrapper(self, *args, **kwargs):
                LogDict.logs.setdefault(id(self), []).append((name, args, kwargs))
                return getattr(super(), name)(*args, **kwargs)
    
            return wrapper
    
        for attr in dir(dict):
            if callable(getattr(dict, attr)):
                if attr in ("fromkeys", "__new__"):  # "classmethod-y"
                    continue
                locals()[attr] = _make_wrapper(attr)
    
        def logrepr(self):
            return "".join(
                "{fun}({rargs}{optsep}{rkwargs})\n".format(
                    fun=fun,
                    rargs=", ".join(repr(arg) for arg in args),
                    optsep=", " if kwargs else "",
                    rkwargs=", ".join(
                        "{} = {}".format(key, value) for key, value in kwargs.items()
                    ),
                )
                for fun, args, kwargs in LogDict.logs[id(self)]
            )
    
    
    d = LogDict()
    d["1"] = 3
    d["1"] += 0.5
    print("1" in d)
    print("log:")
    print(d.logrepr())
    

    This prints the same thing as your solution.

    In my version I also store the log on the class object, then I can avoid the __getattribute__ trickery.