Как считать вызовы методов, а не атрибуты доступа? - PullRequest
0 голосов
/ 21 января 2019

Я готовлю родительский класс для нескольких классов, и с его точки зрения мне нужно знать, был ли вызван конкретный метод экземпляра или нет.

Я начал работать над чем-то вроде этого:

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

Как я могу из экземпляра объекта обнаружить, что его поле вызывается (не только доступно)?

Ответы [ 2 ]

0 голосов
/ 21 января 2019

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

Обратите внимание, что сохранение количества вызовов в экземпляре позволяет только считать вызовы методов экземпляра, очевидно ...

0 голосов
/ 21 января 2019

Это немного «грязно», но обертывание всех методов с помощью функции подсчета может охватить все, что вам нужно:

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

Не должно выдавать ошибок утверждения.

...