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

Time:10-04

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)

  • Related