【问题标题】:How to count method calls, but not attribute access?如何计算方法调用,但不计算属性访问?
【发布时间】:2023-03-19 18:21:01
【问题描述】:

我正在为多个类准备一个父类,从它的角度来看,我需要知道是否调用了特定的实例方法。

我开始做这样的事情:

from collections import defaultdict
class ParentClass:
    def __init__(self, ):
        self.call_count = defaultdict(int)

    def __getattribute__(self, item):
        if item != 'call_count':
            self.call_count[item] += 1
        return object.__getattribute__(self, item)


class ChildClass(ParentClass):
    def child_method(self):
        pass

不幸的是,call_count 还包括对该字段的访问,但没有调用它:

ob = ChildClass()

ob.child_method()
ob.child_method

assert ob.call_count['child_method'] == 1  # it's 2

我怎样才能从对象实例中检测到它的字段正在被调用(不仅仅是访问)?

【问题讨论】:

    标签: python inheritance metaprogramming


    【解决方案1】:

    使用自定义元类的 (python3) 解决方案:

    from collections import defaultdict
    from functools import wraps
    import inspect
    
    def count_calls(func):
        name = func.__name__
    
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            # creates the instance counter if necessary
            counter = getattr(self, "_calls_counter", None)
            if counter is None:
                counter = self._calls_counter = defaultdict(int)
            counter[name] += 1
            return func(self, *args, **kwargs)
    
        wrapper._is_count_call_wrapper = True
        return wrapper
    
    
    class CallCounterType(type):
        def __new__(cls, name, bases, attrs):
            for name, attr in attrs.items():
                if not inspect.isfunction(attr):
                    # this will weed out any callable that is not truly a function
                    # (including nested classes, classmethods and staticmethods)
                    continue
    
                try:
                    argspec = inspect.getargspec(attr)
                except TypeError:
                    # "unsupported callable" - can't think of any callable
                    # that might have made it's way until this point and not
                    # be supported by getargspec but well...
                    continue
    
                if not argspec.args:
                    # no argument so it can't be an instancemethod
                    # (to be exact: a function designed to be used as instancemethod)
                    # Here again I wonder which function could be found here that
                    # doesn't take at least `self` but anyway...
                    continue
    
                if getattr(attr, "_is_count_call_wrapper", False):
                    # not sure why we would have an already wrapped func here but etc...
                    continue
    
                # ok, it's a proper function, it takes at least one positional arg,
                # and it's not already been wrapped, we should be safe
                attrs[name] = count_calls(attr)
    
            return super(CallCounterType, cls).__new__(cls, name, bases, attrs)
    
    
    class ParentClass(metaclass=CallCounterType):
        pass
    
    class ChildClass(ParentClass):
        def child_method(self):
            pass
    

    请注意,在实例上存储调用计数只允许对 instancemethods 调用进行计数,显然...

    【讨论】:

    • 看起来很棒,有据可查,而且工作正常。以下打印1ob = ChildClass(); ob.child_method(); ob.child_method; print(ob._calls_counter['child_method']);。也适用于多个对象,因此在类级别没有存储任何内容。唯一需要改变的可能是公开_calls_counter
    • @AndréC.Andersen 主要取决于预期用途。据我了解,这是框架的一部分,实际上是框架实现的一部分,所以我宁愿保护它(甚至真正私有以避免名称冲突) - 但我们真的没有足够的上下文来说明。
    • 当然,我明白了。我在考虑反映提问者提供的测试代码。但你说得对,我们没有足够的信息来真正知道什么是正确的。
    【解决方案2】:

    以下内容有点“脏”,但是用计数函数包装所有方法可以满足您的需求:

    from collections import defaultdict
    class ParentClass:
        def __init__(self):
            self.call_count = defaultdict(int)
    
            for attr in dir(self):
                if not attr.startswith('__') and attr != '_wrapper_factory':
                    callback = getattr(self, attr)
                    if hasattr(callback, '__call__'):
                        setattr(self, attr, self._wrapper_factory(callback))
    
        def _wrapper_factory(self, callback):
            def wrapper(*args, **kwargs):
                self.call_count[callback.__name__] += 1
                callback(*args, **kwargs)
            return wrapper
    
    class ChildClass(ParentClass):
        def child_method(self):
            pass
    
    
    ob = ChildClass()
    
    ob.child_method()
    ob.child_method
    
    assert ob.call_count['child_method'] == 1
    

    不应给出断言错误。

    【讨论】:

    • @Mirek 子类应该(几乎)始终将其称为父类的初始化程序。但是将这段代码放在初始化器中效率非常低,最好在类级别包装方法(这样你只包装一次),这可以通过类装饰器或自定义元类来完成。
    • @andré : defaultdict(lambda : 0) 可以更简单地表示为defaultdict(int)。类可调用的...
    • @brunodesthuilliers 这听起来很有趣。您能否提供一个答案,说明您将如何在课堂上进行操作。我不确定当您引用对象级别的self.call_count 时这将如何工作。也许是call_count 对象方法?
    • @Mirek 只是清楚地记录了子类必须调用初始化程序,这就足够了。试图“保护”任何东西免受不称职的开发人员的攻击完全是在浪费每个人的时间,即使使用像 Ada 或 Java 这样的“B&D”语言也行不通,所以对于像 Python 这样动态的东西,这不过是一场失败的战斗。
    • @Mirek 示例,添加了元类(它负责为您创建计数器 )。
    猜你喜欢
    • 2018-03-02
    • 2019-03-16
    • 2020-02-13
    • 2018-01-14
    • 1970-01-01
    • 2019-03-01
    • 2017-08-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多