【问题标题】:Detecting object usage in the least invasive and most invisible way以侵入性最小和最不可见的方式检测对象的使用情况
【发布时间】:2023-03-07 08:22:01
【问题描述】:

有没有办法自动检测 Python 对象何时被使用(并可能对此做出反应)?

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

我想以这样一种方式“装饰”我的对象:每当使用它的一种方法,或者每当它的内部状态(成员)发生变化或被访问时,我都会得到一些日志信息,比如"Foo is being used"

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

另外,我将避免直接修改Foo 的类代码,即在其每个方法的开头显式添加print 语句(无论哪种方式,这都不会通知我它的成员何时正在改变)。

而且我不想将我的对象显式注册到其他对象,因为这将是一种“侵入性”方法,需要更改“客户端”代码(使用 Foo 对象的代码) 并且很容易忘记。

【问题讨论】:

    标签: python events decorator dynamic-languages


    【解决方案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

    所有这些都需要你修改客户端代码——如果有办法在不改变客户端代码的情况下实现这一点,我不知道。

    【讨论】:

      【解决方案2】:

      我能想到一个解决方案,它并不完美,但它可能是一个开始。我们可以通过 __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)。这将允许用你的装饰器做更多有趣的事情。

      【讨论】:

        【解决方案3】:

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2010-10-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-11-20
          • 1970-01-01
          • 2016-06-18
          相关资源
          最近更新 更多