繁体   English   中英

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

[英]Access a class attribute inside a python class 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实例正在传递,所以我想确保每个函数正确打印日志名称(我只是在使用类名称)。 因此,我尝试装饰每个类的所有方法,以便记住先前的日志名称,然后将日志名称设置为该类的名称,运行该方法,最后将日志名称设置回什么以前是。 我从这里使用装饰器/描述符。 为了简洁起见,我将仅发布对它的更改。 我重命名了装饰器setlogger,在descript类的每个方法内添加了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()吗?

我认为您正在尝试以错误的方式解决错误的问题。 但是我可以解释为什么您的代码没有执行您要使其执行的操作。


首先,在装饰器中执行以下操作:

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的类属性,即类本身的一个属性,该类的所有实例都共享该属性。 但是类中的所有代码都使用实例属性,每个实例都有自己的副本。 这就是在__init__分配self.log时得到的。 因此,没有名为log类属性,这意味着您将获得以下信息:

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

您当然可以创建一个class属性……但是这样做没有任何用处。 具有相同名称的instance属性将对其进行阴影处理。

您需要访问inner的instance属性,这意味着您需要一个self才能对其进行访问。 而且您显然没有在setlogger拥有self 但是考虑一下您在做什么:您正在将一个方法与另一个方法包装在一起。 方法将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

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


同样,我认为这不是一个好的解决方案。 但是它将完成您想要做的事情。

我仍然不完全了解您要解决的问题,但是我想是这样的:

构造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):
        # ...

当正在初始化的ClassAself.__class__将是"ClassA" ,而不是"LogUserMixin" ,所以这正是您想要的。 即使您的真实类已经具有基类或子类的层次结构,或者它们在__init__进行了其他工作或使用了其他参数,该方法仍然有效。 在某些情况下,您只需要多做一点工作即可。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM