Обнаружение использования объекта наименее инвазивным и наиболее невидимым способом - PullRequest
1 голос
/ 29 марта 2019

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

Например, допустим, у меня есть объект типа Foo. Я не написал код класса для Foo, поскольку он поступает из внешней библиотеки.

Я хотел бы «украсить» мой объект таким образом, чтобы при каждом использовании одного из его методов или при изменении или обращении к его внутреннему состоянию (элементам) я получал некоторую информацию о регистрации, например, "Foo is being used".

Я использую термин «украшать», чтобы подчеркнуть, что я не хотел бы менять все интерфейсы, в которых используются объекты типа Foo. Я просто хотел бы добавить некоторые функциональные возможности к нему.

Также я бы избежал необходимости напрямую вмешиваться в код класса Foo, т. Е. Путем явного добавления оператора print в начале каждого из его методов (в любом случае это не сообщило бы мне, когда его члены меняются).

И я не хотел бы явно регистрировать свои объекты для некоторых других объектов, поскольку это был бы «инвазивный» подход, который потребовал бы изменения кода «на стороне клиента» (кода, использующего объекты Foo) ) и это было бы то, что можно легко забыть.

Ответы [ 3 ]

1 голос
/ 29 марта 2019

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

import re

dunder_pattern = re.compile("__.*__")
protected_pattern = re.compile("_.*")

def is_hidden(attr_name):
    return dunder_pattern.match(attr_name) or protected_pattern.match(attr_name)


def attach_proxy(function=None):
    function = function or (lambda *a: None)

    def decorator(decorated_class):

        class Proxy(decorated_class):
            def __init__(self, *args, **kwargs):
                function("init", args, kwargs)
                super().__init__(*args, **kwargs)

            def __getattribute__(self, name):
                if not is_hidden(name):
                    function("acces", name)
                return object.__getattribute__(self, name)

            def __getattr__(self, name):
                if not is_hidden(name):
                    function("acces*", name)
                return object.__getattr__(self, name)

            def __setattribute__(self, name, value):
                if not is_hidden(name):
                    function("set", name, value)
                return object.__setattribute__(self, name, value)

            def __setattr__(self, name, value):
                if not is_hidden(name):
                    function("set*", name, value)
                return object.__setattr__(self, name, value)

        return Proxy

    return decorator

Что вы можете использовать для украшения своего класса:

@attach_proxy(print)
class A:
    x = 1
    def __init__(self, y, msg="hello"):
        self.y = y

    @classmethod
    def foo(cls):
        print(cls.x)

    def bar(self):
        print(self.y)

Что приведет к следующему:

>>> a = A(10, msg="test")
init (10,) {'msg': 'test'}
set* y 10
>>> a.bar()
acces bar
acces y
10
>>> a.foo() # access to x is not captured
acces foo
1
>>> y = a.y
acces y
>>> x = A.x # access to x is not captured
>>> a.y = 3e5
set* y 300000.0

Проблемы:

  1. Доступ к атрибутам класса не фиксируется (для этого нужен метакласс, но я не вижу способа сделать это на лету).

  2. Тип A скрыт (за типом Proxy), это, вероятно, проще решить:

>>> A
__main__.attach_proxy.<locals>.decorator.<locals>.Proxy

С другой стороны, это не обязательно проблема, так как она будет работать, как и ожидалось:

>>> a = A(10, msg="test")
>>> isinstance(a, A)
True

Edit обратите внимание, что я не передаю экземпляры вызовам function, но это было бы хорошей идеей, заменяя вызовы с function("acces", name) на function("acces", self, name). Это позволило бы делать намного больше забавных вещей с вашим декоратором.

1 голос
/ 29 марта 2019

Вы можете использовать monkey patching для достижения этой цели. Повторно назначьте одну из функций-членов на объекте как декорированную функцию, которая, в свою очередь, вызывает исходную функцию вместе с добавлением некоторой регистрации.

Например:

a = Test() # An object you want to monitor
a.func() # A specific function of Test you want to decorate

# Your decorator
from functools import wraps
def addLogging(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        print 'Calling {}'.format(function.func_name)   
        return function(*args, **kwargs)
    return wrapper

a.func = addLogging(a.func)

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

Что касается определения, когда значение переменной-члена изменяется, вы можете обратиться к this .

Все это требует от вас изменения кода на стороне клиента - если есть способ добиться этого без изменения кода клиента, я не знаю об этом.

0 голосов
/ 29 марта 2019

Вы можете объединить ответ, предоставленный @suicidalteddy, с проверкой метода, что приведет к чему-то похожему на следующее:

# Your decorator
def add_logging(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
        print 'Calling {}'.format(function.func_name)   
        return function(*args, **kwargs)
    return wrapper

instance = Test() # An object you want to monitor

# list of callables found in instance
methods_list = [
   method_name for method_name in dir(instance) if callable(
      getattr(instance, method_name)
   )
]

# replaces original method by decorated one
for method_name in methods_list:
    setattr(instance, method_name, add_logging(getattr(instance, method_name))

Я не проверял это, но что-то похожее должно сработать, удачи!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...