以最小侵入性和最不可见的方式检测对象的使用情况

时间:2019-03-29 08:53:16

标签: python events decorator dynamic-languages

有没有一种方法可以自动检测何时使用了python对象(并且可能对此做出反应)?

例如,假设我有一个Foo类型的对象。我没有为Foo编写类代码,因为它来自外部库。

我想以一种这样的方式“装饰”我的对象:无论何时使用它的一种方法,或者只要它的内部状态(成员)发生变化或被访问,我都会得到一些日志信息,例如{{1} }。

我使用“装饰”一词来强调我不想更改使用"Foo is being used"类型对象的所有接口。我只是想为其添加一些功能。

我也避免了直接修改Foo的类代码的方法,即通过在每个方法的开头显式添加Foo语句(这两种方法都不会通知我)成员何时更改)。

我不想将我的对象显式注册到其他对象,因为那是一种“侵入式”方法,需要更改“客户端”代码(使用{{1}的代码}个对象),而这很容易被遗忘。

3 个答案:

答案 0 :(得分:1)

您可以使用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

所有这一切都需要您修改客户端代码-如果有一种方法可以在不更改客户端代码的情况下实现此目的,我不知道。

答案 1 :(得分:1)

我可以想到一个解决方案,它虽然不完美,但可能只是一个开始。我们可以在将从装饰的类继承的类中通过__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

编辑请注意,我不会将实例传递给function调用,但这实际上是一个好主意,将调用从function("acces", name)替换为function("acces", self, name) 。这将使您的装饰器制作出更多有趣的东西。

答案 2 :(得分:0)

您可以将@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))

我还没有测试过,但是类似的东西应该可以工作,祝您好运!