【问题标题】:how to make a derived class that logs all access to its members?如何创建一个派生类来记录对其成员的所有访问?
【发布时间】:2021-10-03 09:09:26
【问题描述】:

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

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

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

注意:这不是this question 的重复,因为其中的问题是如何避免无限递归,而不是如何自动化/简化编写派生类的过程。

【问题讨论】:

    标签: python python-3.x oop metaprogramming


    【解决方案1】:

    你可以只自动生成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())
    

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

    在我的版本中,我还将日志存储在类对象上,然后我可以避免 __getattribute__ 诡计。

    【讨论】:

    • 你经常在课堂上使用这个for循环吗?我以前从未见过。您能否进一步扩展使用(当它很好时)或添加一些参考?这真的很酷。
    • @PeterTrcka 类主体中的内容与python中的大多数其他代码一样执行,主要区别在于它的执行时间及其上下文:当您创建给定类的实例时,以及在正在创建的对象(意味着像“init”这样的所有本地名称实际上都是正在实例化的对象的属性)。使用这种循环往往会混淆你的代码,因为你看不到你定义的名字,所以如果有其他选择,你应该尽量避免它。查看“类实例化”、“新型类”和“元类”以了解更多信息。
    • @L3viathan 非常感谢,这就是我想要的(没想到使用循环!)。我想澄清一下,我确实想保存参数的字符串表示,而不是参数本身:如果我执行一些操作,例如将 dict 值设置为空列表,然后将空列表更改为 [3],使用您的代码我会将第一个分配视为__setitem__('key', [3]) 而不是__setitem__('key', [])
    • @sortai 在这种情况下,将reprification 移动到我们附加到日志的位置。
    • @PeterTrcka 差不多就是sortai所说的。我不经常使用它;这里有几件事我会尽量避免,包括与locals() 的结果进行交互。
    【解决方案2】:

    我会选择不同的方法。

    我创建了简单的装饰器类,称为EventLogger。现在您的LogDict 将从此类继承,因此事件日志将成为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)
    
    

    【讨论】:

    • 虽然这实现了我用更短的代码所做的事情,但我仍然需要手动重载我想要记录的每个方法,这不是我感兴趣的事情。还是谢谢。
    • 明白。您可以按照@L3viathan 的建议使用for 循环。我的解决方案最大的好处是易于实施其他记录器。如果不需要创建其他记录器来手动装饰每个方法是压倒性的。
    猜你喜欢
    • 2012-03-05
    • 2017-12-15
    • 2017-08-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多