如何创建一个派生的 class 来记录对其成员的所有访问?

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

我正在尝试制作一个行为类似于字典的 class,除了任何时候调用其方法之一或访问其属性之一时,都会记录该事实。 我将通过展示我所做的幼稚实现来阐明我的意思(重复代码替换为省略号):

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

在这里,至少对于我重载的方法,我正在保存正在调用的方法及其参数的 repr(如果我只保存参数,我 运行 有看到最新的风险可变对象的“版本”而不是旧对象)。 这个实现有点管用:

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

产生:

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

但是它相当笨拙,我不确定我是否涵盖了所有可能的方法。 有没有更有效的方法来做到这一点,理想情况下可以推广到任何给定的 class(并且包装和记录所有 dunder 方法,而不仅仅是可见的方法)?

注意:这不是 this question 的副本,因为其中的问题是如何避免无限递归,而不是如何 automate/simplify 编写派生 class 的过程.

我会选择不同的方法。

我创建了简单的装饰器 class,名为 EventLogger。现在您的 LogDict 将从这个 class 继承,因此事件日志将成为 LogDict 的一部分。如果你想记录事件,你可以简单地使用 @EventLogger.log.

装饰你想跟踪的方法

如果需要,您可以使用其他日志记录功能扩展此 EventLogger。如果在某些方法中想要跟踪其他细节,例如运行 时间的时间,或者将数据记录到其他日志,你可以轻松做到。

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)

你可以自动生成 dict 的所有方法(有一些例外),然后你就不必重复这么多:

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())

这会打印与您的解决方案相同的内容。

在我的版本中,我还将日志存储在 class 对象上,这样我就可以避免 __getattribute__ 欺骗。