访问python类装饰器中的类属性

时间:2014-01-19 04:20:11

标签: python class decorator

编辑 :我找到了this method decorator,并且能够使用它来单独包装ClassA和ClassB的方法(省略__init__)。但是,我不想手动包装单个方法,而是只想包装该类。

我创建了自己的日志记录类MyLogger,它继承了logging.Logger。在这个类中,(除其他外)我有一个FileHandler在其输出中打印记录器名称:

import logging

class MyLogger(logging.Logger):
    def __init__(self, name, path="output.log"):
        logging.Logger.__init__(self, name, logging.DEBUG)
        logpath = path

        fh = logging.FileHandler(logpath)
        fh.setLevel(logging.DEBUG)
        fh.setFormatter(logging.Formatter("%(name)s - %(message)s"))

        # stream handler omitted

        self.addHandler(fh)

我也有ClassA和ClassB,它们都获得了MyLogger的相同实例:

class ClassA(object):
    def __init__(self, mylogger):
        self.log = mylogger

    def fn1(self):
        self.log.debug("message1 from ClassA fn1")
        self.fn2()
        b = ClassB(self.log)
        b.fn1()
        self.log.debug("message2 from ClassA fn1")

    def fn2(self):
        self.log.debug("message1 from ClassA fn2")

    # many more functions

class ClassB(object):
    def __init__(self, mylogger):
        self.log = mylogger

    def fn1(self):
        self.log.debug("message1 from ClassB fn1")

    # many more functions

这是一个简单的“主要”功能:

print "inside main"
log = MyLogger("main")
a = ClassA(log)
a.fn1()

因为正在传递MyLogger实例,所以我想确保每个函数正确打印日志名称(我只是使用类名)。所以我试图装饰每个类的所有方法,以便记住以前的日志名称,然后将日志名称设置为类的名称,运行该方法,最后将日志名称设置回什么以前是。我正在使用here中的装饰器/描述符。为了简洁起见,我只会发布我的更改。我重命名了装饰器setlogger,在describe类的每个方法中添加了print语句,并更改了make_bound,如下所示:

def make_bound(self, instance):
   print "in __BOUND__"
   @functools.wraps(self.f)
   def wrapper(*args, **kwargs):
       '''This documentation will disapear :)'''
       prev = instance.log.name
       print "about to wrap %s.%s, prev = %s" % (instance.__class__.__name__, self.f.__name__, prev)
       ret = self.f(instance, *args, **kwargs)
       instance.log.name = prev
       print "done wrapping %s.%s, now = %s" % (instance.__class__.__name__, self.f.__name__, prev)
       return ret
   # This instance does not need the descriptor anymore,
   # let it find the wrapper directly next time:
   setattr(instance, self.f.__name__, wrapper)
   return wrapper

如果我使用setlogger装饰器/描述符来包装ClassA和ClassB中的各个方法,它可以正常工作。但是,我想把这两个类包起来。所以这是我的班级装饰师:

def setloggerforallmethods(cls):
    def decorate(*args, **kwargs):
        for name, m in inspect.getmembers(cls, inspect.ismethod):
            if name != "__init__":
                print "calling setattr on %s.%s" % (cls.__name__, name)
                setattr(cls, name, setlogger(m))
        return cls

    return decorate

如果我用@setloggerforallmethods包装ClassA和ClassB,并运行main函数,继承输出:

inside main
calling setattr on ClassA.fn1
in __INIT__: f = fn1
calling setattr on ClassA.fn2
in __INIT__: f = fn2
in __GET__
in __UNBOUND__
Traceback (most recent call last):
  File "/ws/maleva-rcd/yacht/classa.py", line 23, in <module>
    a.fn1()
  File "/ws/maleva-rcd/yacht/yachtlogger.py", line 34, in wrapper
    self.f.__name__)
ValueError: zero length field name in format

我不明白为什么fn1此时没有绑定。是不是像a.fn1()中那样绑定了?

2 个答案:

答案 0 :(得分:0)

我认为你试图以错误的方式解决错误的问题。但我可以解释为什么你的代码没有做你想做的事情。


首先,在你的装饰者中,你这样做:

for name, fn in inspect.getmembers(cls, inspect.ismethod):
    if name != "__init__":
        print "calling setlogger on %s" % cls.__name__ + "." + name
        fn = setlogger(fn)

这没有效果。对于每个绑定方法fn,您创建一个包装函数,然后将局部变量fn重新绑定到该函数。这没有比这样做更有效:

def foo(a):
    a = 3
i = 0
foo(i)

如果要在类上设置属性,则必须在类上设置属性,如下所示:

setattr(cls, name, setlogger(fn))

现在你的包装将被调用。


接下来,cls.log是一个名为log的类属性 - 即类本身的属性,由该类的所有实例共享。但是类中的所有代码都使用 instance 属性,其中每个实例都有自己的副本。这就是您在self.log中分配__init__时获得的结果。因此,没有名为log的类属性,这意味着您只需要这样:

AttributeError: type object 'ClassA' has no attribute 'log'

你当然可以创建一个类属性......但这不会有任何好处。同名的实例属性只会遮蔽它。

您需要访问inner中的实例属性,这意味着您需要self才能访问它。你显然在self内没有setlogger。但想想你正在做什么:你用另一种方法包装方法。方法将self作为第一个参数。事实上,如果您修改inner以打印其args,您会看到第一个总是类似<__main__.ClassA object at 0x12345678>。所以:

def inner(self, *args, **kwargs):
    prevname = self.log.name
    self.log.name = cls.__name__
    ret = func(self, *args, **kwargs) # don't forget to forward self
    self.log.name = prevname
    return ret

但是,如果这些包装方法中的任何一个引发异常,它们将使名称处于错误状态。实际上,您需要创建用于存储和恢复值的上下文管理器,或者只需try / finally。这也恰好使包装器更容易编写:

def inner(self, *args, **kwargs):
    prevname = self.log.name
    self.log.name = cls.__name__
    try:
        return func(self, *args, **kwargs)
    finally:
        self.log.name = prevname

最后,您需要删除每个self.log.name =方法中的__init__。否则,当您在B的中间构造A.fn1实例时,您将更改记录器的名称,而无需通过恢复以前名称的包装器。


同样,我认为这不是一个好的解决方案。但它会做你想做的事。

答案 1 :(得分:0)

我仍然不完全理解你要解决的问题,但我认为就是这样:

构造MyLogger需要两条信息:名称和路径。您不希望每个班级都必须知道该路径。所以,你认为你需要共享MyLogger实例,因为没有别的办法。然后,因为MyLogger将其名称存储为属性,所以必须在每个方法的包装器中修改该属性。

但是有一个更简单的方法:让你的类成为一个“记录器工厂” - 也就是说,一个可调用的构造适合它们的记录器 - 而不是记录器。 MyLogger类本身已经 这样一个可调用的,因为它采用path的默认值而你只是使用它。但是让我们假装这不是真的,你想要使用一些非默认的path。还是很容易;你只需要把它包起来:

class ClassA(object):
    def __init__(self, log_factory):
        self.log_factory = log_factory
        self.log = log_factory("ClassA")
    def fn1(self):
        # ...
        b = ClassB(self.log_factory)
        # ...

class ClassB(object):
    def __init__(self, log_factory):
        self.log_factory = log_factory
        self.log = log_factory("ClassB")
    # ...

# or just log_factory = functools.partial(MyLogger, log="output.log")
def log_factory(name):
    return MyLogger(name, "output.log")
a = ClassA(log_factory)
a.fn1()

您可能会注意到两个类中的__init__方法都做同样的事情。那么,为什么不将它提取到mixin基类中呢?

class LogUserMixin(object):
    def __init__(self, log_factory):
        self.log_factory = log_factory
        self.log = log_factory(self.__class__.__name__)

现在:

class ClassA(LogUserMixin):
    def fn1(self):
        # ...

ClassA被初始化时,self.__class__将是"ClassA",而不是"LogUserMixin",所以这完全符合您的要求。即使你的真实类已经有了基类,或者子类的层次结构,或者它们在__init__中做了其他的东西,或者采取了其他的参数,它也能工作。你只需要在其中一些案例中做更多的工作。

相关问题