【问题标题】:Access a class attribute inside a python class decorator访问 python 类装饰器中的类属性
【发布时间】:2014-02-08 09:25:29
【问题描述】:

编辑:我找到了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,在 descript 类的每个方法中添加了打印语句,并将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()那样绑定到a吗?

【问题讨论】:

  • 首先,这显然是 Python 2.x,因此您应该始终使用 class A(object): 创建新样式的类。您不希望在尝试调试的任何内容之上添加经典类的怪癖。
  • 第二,你不应该明确地调用logging.Logger.__init__。如果你想构造一个 Logger,你可以像任何其他类型一样做:调用类作为构造函数。 (如果您的意思是让 MyLogger 成为 logging.Logger 的子类,那就不同了……但是您实际上必须将其设为子类。)这意味着您的代码甚至无法运行并且表现出你所询问的行为,它只是得到一个 TypeError 试图传递一个非 Logger 作为 self 参数 logging.Logger.__init__
  • 最后,你的ClassB 中没有任何东西可以初始化一个新的记录器,或者重新初始化一个现有的记录器,所以你问的问题不可能存在于你的代码中。
  • 另外,self.log 不是类属性,它是实例属性。这意味着每个ClassA 对象(以及每个ClassB 对象,不用说)都有一个不同的对象。而只是做fn = setlogger(fn) 并没有做任何有用的事情,它只是创建了一个包装函数,将它绑定到本地名称fn,然后忘记它。如果你想对一个对象的属性进行monkeypatch,你必须在对象上实际调用setattr
  • @abarnert:如果我理解他的例子,他会预先创建一个记录器并将其作为参数传递给构造函数,所以同一个对象将被共享为self.log

标签: python class decorator


【解决方案1】:

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


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

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 属性,其中每个实例都有自己的副本。这就是您在 __init__ 中分配 self.log 时得到的结果。所以,没有名为log 的类属性,这意味着你只会得到这个:

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

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

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

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 实例时,您将更改记录器的名称,而无需通过恢复先前名称的包装器。


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

【讨论】:

  • 看起来我们的帖子交叉了。上面的描述符适用于单独包装类方法,但我从您的回复中收集到的是,可以在没有描述符的情况下执行此操作。我将尝试显式使用 self arg 以及 try finally。
  • 另外,如果我说错了,请提出更好的方法。我想要的是多个类使用记录器将信息、警告等记录到控制台并将调试记录到 same 文件。记录到文件的格式包括记录它的类的名称
【解决方案2】:

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

构造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__ 中做了额外的事情,或者采用额外的参数,它也可以工作;在某些情况下,您只需要多做一点工作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-17
    • 1970-01-01
    • 2020-07-09
    • 1970-01-01
    • 2015-08-23
    • 2017-03-28
    • 2018-10-06
    • 2020-05-14
    相关资源
    最近更新 更多