如何在运行时包装Python实例方法

4
我希望在某些类中进行调用时记录调试信息。记录的数据包括:
  • 函数名称
  • 函数被调用的堆栈跟踪
  • 函数执行所需的时间
  • 传递给函数的args和kwargs参数
我希望这个包装器比较通用。同时,包装应该在运行时进行。我已经创建了以下包装器类,它记录了信息并将调用委托给原始实例。
import datetime
import traceback
from functools import partial

from logging_module import db_log


class BaseWrapper(object):
    def __init__(self, item):
        self._item = item

    def __getattr__(self, attr):
        return getattr(self._item, attr)


class DBLogWrapper(BaseWrapper):

    @staticmethod
    def _time_method(method):
        name = "{0}.{1}.{2}".format(
            method.im_class.__module__,
            method.im_class.__name__,
            method.__name__
        )

        def timed_method(self, *args, **kwargs):
            begin = datetime.datetime.now()
            return_val = method.im_func(self, *args, **kwargs)
            end = datetime.datetime.now()

            trace = traceback.format_stack()

            db_log(
                name,
                begin,
                end,
                info={
                    'args': args,
                    'kwargs': kwargs,
                    'trace': trace
                }
            )
            return return_val

        return timed_method

    def __init__(self, item, methods):
        super(DBLogWrapper, self).__init__(item)
        for method in methods:
            class_method = getattr(item, method)
            wrapped_method = DBLogWrapper._time_method(class_method)
            wrapped_method = partial(wrapped_method, self._item)
            setattr(self, method, wrapped_method)

示例用法:

class MyClass(object):

    def hello(self, greeting):
        print greeting

    def goodbye(self):
        print 'Good Bye'

a = MyClass()

if DEBUG:
    a = DBLogWrapper(a, ['hello'])

a.hello()
a.goodbye()

在这种情况下,hello的调用将被记录,但goodbye的调用不会被记录。
然而,对于这个看起来应该很简单的任务来说,这似乎有些过度。我正在寻求如何改进上述代码或完全不同的方法来解决它。

这有什么过度杀伤力的问题吗? - Waleed Khan
整个解决方案对我来说似乎有些复杂。使用部分委托包装器使实例方法按预期工作。感觉有点太繁琐了。 - Drover
为什么不直接编写一个装饰器并将其应用于您想要跟踪的方法呢?我认为这样更简单明了。 - Bakuriu
时间装饰器已经是一个很好的选择。但是,我不想仅仅为类方法添加装饰器,原因有几个。首先,我无法访问我想要跟踪的方法。它们在第三方模块中定义。其次,我希望这些装饰器根据某些标志在运行时应用。我不想一直使用它们。 - Drover
2个回答

4
你正在做太多的工作。你根本不需要使用 partial。只需定义没有 self 参数的 timed_method 并直接调用 method 即可:
import datetime
import traceback
from functools import partial

def db_log(*args, **kwargs): print args, kwargs # Mock


class BaseWrapper(object):
    def __init__(self, instance):
        self._instance = instance

    def __getattr__(self, attr):
        return getattr(self._instance, attr)


class DBLogWrapper(BaseWrapper):

    @staticmethod
    def _time_method(method):
        name = "{0}.{1}.{2}".format(
            method.im_class.__module__,
            method.im_class.__name__,
            method.__name__
        )

        def timed_method(*args, **kwargs):
            begin = datetime.datetime.now()
            return_val = method(*args, **kwargs)
            end = datetime.datetime.now()

            trace = traceback.format_stack()

            db_log(
                name,
                begin,
                end,
                info={
                    'args': args,
                    'kwargs': kwargs,
                    'trace': trace
                }
            )
            return return_val

        return timed_method

    def __init__(self, instance, methods):
        super(DBLogWrapper, self).__init__(instance)
        for method in methods:
            class_method = getattr(instance, method)
            wrapped_method = DBLogWrapper._time_method(class_method)
            setattr(self, method, wrapped_method)

输出:

>>> a = MyClass()
>>> a = prova.DBLogWrapper(a, ['hello'])
>>> a.hello()
A
('__main__.MyClass.hello', datetime.datetime(2013, 1, 17, 20, 48, 26, 478023), datetime.datetime(2013, 1, 17, 20, 48, 26, 478071)) {'info': {'args': (), 'trace': ['  File "<stdin>", line 1, in <module>\n', '  File "prova.py", line 31, in timed_method\n    trace = traceback.format_stack()\n'], 'kwargs': {}}}
>>> a.goodbye()
B

无论如何,您可能可以使用一些__getattr__魔术来代替,例如:
class DBLogWrapper2(BaseWrapper):

    def __init__(self, instance, methods):
        super(DBLogWrapper, self).__init__(instance)

        self._methods = methods

    def __getattr__(self, attr):
        if attr not in methods:
            return getattr(self._instance, attr)

        def f(*args, **kwargs):
            return self.timed_method(getattr(self._item, attr),
                                     *args, **kwargs)
        return f

    def timed_method(method, *args, **kwargs):
        begin = datetime.datetime.now()
        return_val = method(*args, **kwargs)
        end = datetime.datetime.now()

        trace = traceback.format_stack()

        db_log(name,
            begin,
            end,
            info={
                'args': args,
                'kwargs': kwargs,
                'trace': trace
            }
        )
        return return_val

啊,该方法已经绑定到该项!太棒了,这样可以使它更清晰。至于__getattr__魔法,我想尝试避免每次调用时都创建定时方法。我猜我可以让它变成懒加载,并在创建完包装方法后将其存储在self.__dict__中。无论哪种方式,这都非常有帮助。谢谢。 - Drover
@Drover 没错。方法是使用getter定义的,因此如果您从类中访问它们,则不会绑定,但是通过实例访问它们会绑定“self”参数。阅读此文档以获得更好的解释。 - Bakuriu

1
我简化了上面的答案。在我的情况下,你也不需要返回实例,这一点很重要,因为我无法访问它在哪里被使用。
def method_wrapper(method):
    def method_wrap(*args, **kwargs):
        print('Wrapper Code: ', end='')
        return_val = method(*args, **kwargs)
        return return_val

    return method_wrap

def wrap_it(wrapper, instance, method):
    class_method = getattr(instance, method)
    wrapped_method = wrapper(class_method)
    setattr(instance, method, wrapped_method)

class MyClass:

    def hello(self, greeting):
        print(greeting)

    def goodbye(self):
        print('Good Bye')

a = MyClass()

wrap_it(method_wrapper, a, 'hello')

a.hello('hi')
a.goodbye()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接