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.
CodePudding user response:
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.
CodePudding user response:
I will choose different approach.
I have created simple decorator class, called EventLogger
. Now your LogDict
will inherit from this class so the log of events will be part of LogDict
. If you want to log events, you can just simply decorate the methods which you want to track using @EventLogger.log
.
If you need you extend this EventLogger
with other logging functions. If in some methods want to track other details e.g. time of running time, or log data to other log you can do it with ease.
from functools import wraps
class EventLogger:
_logged_events = list()
@property
def logged_events(self):
return self._logged_events
def log(func):
@wraps(func)
def wrapped(self, *args, **kwargs):
self.__to_logger(self, func_name=func.__name__, *args, **kwargs)
return func(self, *args, **kwargs)
return wrapped
def __to_logger(self, *args, **kwargs):
func_name = kwargs.pop('func_name')
args = args[1:] # first param is self
# TODO: implement the logging format
self._logged_events.append(
dict(func=func_name,
args=args,
kwargs=kwargs)
)
class LogDict(dict, EventLogger):
@EventLogger.log
def __init__(self, *args, **kwargs):
return super().__init__(*args, **kwargs)
@EventLogger.log
def __setitem__(self, key, value):
return super().__setitem__(key, value)
@EventLogger.log
def __getitem__(self, key):
return super().__getitem__(key)
ld = LogDict(a=10)
ld['aa'] = 5
print(ld)
print(ld.logged_events)