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.
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.