[英]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):
# ...
当正在初始化的ClassA
, self.__class__
将是"ClassA"
,而不是"LogUserMixin"
,所以这正是您想要的。 即使您的真实类已经具有基类或子类的层次结构,或者它们在__init__
进行了其他工作或使用了其他参数,该方法仍然有效。 在某些情况下,您只需要多做一点工作即可。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.