简体   繁体   English

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

[英]Access a class attribute inside a python class decorator

EDIT : i found this method decorator and was able to use it to individually wrap the methods (omitting __init__ ) of ClassA and ClassB. 编辑 :我发现此方法装饰器 ,并能够使用它来单独包装ClassA和ClassB的方法(省略__init__ )。 however, instead of manually wrapping individual methods, i'd like to just wrap the class. 但是,我不想只包装单个方法,而是只包装类。

i've created my own logging class, MyLogger, which inherits logging.Logger. 我创建了自己的日志记录类MyLogger,该类继承了logging.Logger。 in this class, (among other things) i have a FileHandler which prints the logger name in its output: 在此类中(除其他事项外),我有一个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)

i also have ClassA and ClassB, which both get the same instance of MyLogger: 我也有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

here's a simple "main" function: 这是一个简单的“主要”功能:

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

because the MyLogger instance is being passed around, i'd like to ensure the log name (i'm just using the class name) is printed correctly by each function. 因为MyLogger实例正在传递,所以我想确保每个函数正确打印日志名称(我只是在使用类名称)。 so i'm attempting to decorate all methods of each class so that the the previous log name is remembered, then the log name is set to the name of the class, the method is run, and finally the log name is set back to what it previously was. 因此,我尝试装饰每个类的所有方法,以便记住先前的日志名称,然后将日志名称设置为该类的名称,运行该方法,最后将日志名称设置回什么以前是。 i'm using the decorator/descriptor from here . 我从这里使用装饰器/描述符。 for the sake of brevity, i will only post my changes to it. 为了简洁起见,我将仅发布对它的更改。 i renamed the decorator setlogger, have added print statements inside each method in the descript class, and have altered make_bound as follows: 我重命名了装饰器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

if i use the setlogger decorator/descriptor to wrap individual methods in ClassA and ClassB, it works fine. 如果我使用setlogger装饰器/描述符将单个方法包装在ClassA和ClassB中,则效果很好。 however, i'd like to just wrap the two classes. 但是,我只想包装两个类。 so here's my class decorator: 所以这是我的课堂装饰:

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

if i wrap ClassA and ClassB with @setloggerforallmethods , and run the main function, heres the output: 如果我用@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

i dont understand why fn1 is unbound at this time. 我不明白为什么此时fn1没有绑定。 isnt it bound to a as in a.fn1() ? 它不是绑定到a.fn1()吗?

I think you're trying to solve the wrong problem in the wrong way. 我认为您正在尝试以错误的方式解决错误的问题。 But I can explain why your code isn't doing what you're trying to make it do. 但是我可以解释为什么您的代码没有执行您要使其执行的操作。


First, in your decorator, you do this: 首先,在装饰器中执行以下操作:

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

That has no effect. 那没有效果。 For each bound method fn , you create a wrapper function, then rebind the local variable fn to that function. 对于每个绑定方法fn ,创建一个包装函数,然后将局部变量fn重新绑定到该函数。 That has no more effect than doing this: 这样做没有什么效果:

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

If you want to set an attribute on the class, you have to set an attribute on the class, like this: 如果要在类上设置属性,则必须在类上设置属性,如下所示:

setattr(cls, name, setlogger(fn))

Now your wrapper will get called. 现在,您的包装器将被调用。


Next, cls.log is a class attribute named log —that is, an attribute on the class itself, which is shared by all instances of that class. 接下来, cls.log是一个名为log的类属性,即类本身的一个属性,该类的所有实例都共享该属性。 But all of the code within the classes uses instance attributes, where each instance has its own copy. 但是类中的所有代码都使用实例属性,每个实例都有自己的副本。 That's what you get when you assign self.log in your __init__ . 这就是在__init__分配self.log时得到的。 So, there is no class attribute named log , meaning you'll just get this: 因此,没有名为log类属性,这意味着您将获得以下信息:

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

You could of course create a class attribute… but that won't do any good. 您当然可以创建一个class属性……但是这样做没有任何用处。 The instance attribute of the same name will just shadow it. 具有相同名称的instance属性将对其进行阴影处理。

You need to access the instance attribute inside inner , which means you need a self to access it off. 您需要访问inner的instance属性,这意味着您需要一个self才能对其进行访问。 And you obviously don't have self inside setlogger . 而且您显然没有在setlogger拥有self But think about what you're doing: you're wrapping a method with another method. 但是考虑一下您在做什么:您正在将一个方法与另一个方法包装在一起。 Methods get self as their first argument. 方法将self作为其第一个参数。 In fact, if you modify inner to print out its args , you'll see that the first one is always something like <__main__.ClassA object at 0x12345678> . 实际上,如果您修改inner来打印其args ,则会看到第一个总是类似于<__main__.ClassA object at 0x12345678> So: 所以:

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

But if any of these wrapped methods ever raises an exception, they'll leave the name in the wrong state. 但是,如果这些包装的方法中的任何一个引发异常,它们将使名称保持错误状态。 So really, you need to either create a context manager for stashing and restoring the value, or just a try / finally . 因此,实际上,您需要创建一个用于存储和还原值的上下文管理器,或者只是一个try / finally Which also happens to make the wrapper a little easier to write: 这也使包装器更容易编写:

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

Finally, you need to remove the self.log.name = in each __init__ method. 最后,您需要在每个__init__方法中删除self.log.name = Otherwise, when you construct a B instance in the middle of A.fn1 , you're changing the logger's name without going through the wrapper that restores the previous name. 否则,当您在A.fn1的中间构造一个B实例时,您将更改记录器的名称,而无需通过用于恢复先前名称的包装器。


Again, I don't think this is a good solution. 同样,我认为这不是一个好的解决方案。 But it will do what you're trying to do. 但是它将完成您想要做的事情。

I still don't completely understand the problem you're trying to solve, but I think it's this: 我仍然不完全了解您要解决的问题,但是我想是这样的:

Constructing a MyLogger takes two pieces of information: a name, and a path. 构造MyLogger需要两条信息:名称和路径。 You don't want every class to have to know that path. 您不希望每个班级都必须知道那条路。 So, you figured you needed to share the MyLogger instance, because there's no other way around that. 因此,您认为需要共享MyLogger实例,因为没有其他方法可以解决此问题。 And then, because the MyLogger stores its name as an attribute, you had to hack up that attribute in wrappers around every method. 然后,由于MyLogger将其名称存储为属性,因此您必须在每种方法的包装器中修改该属性。

But there is a much simpler way around that: Make your classes take a "logger factory"—that is, a callable which constructs an appropriate logger for them—instead of a logger. 但是,有一个更简单的方法:使您的类采用“记录器工厂”(即,为该类构造合适的记录器的可调用对象)而不是记录器。 The MyLogger class itself already is such a callable, since it takes a default value for path and you just use it. MyLogger类本身已经可调用的,因为它采用path的默认值,而您只是使用它。 But let's pretend that weren't true, and you wanted to use some non-default path . 但是,让我们假装这不是真的,而您想使用一些非默认path Still easy; 仍然很容易; you just need to wrap it up: 您只需要包装一下即可:

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()

You may notice that the __init__ method in both classes does the same thing. 您可能会注意到,两个类中的__init__方法都做同样的事情。 So, why not extract it into a mixin base class? 那么,为什么不将其提取到mixin基类中呢?

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

Now: 现在:

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

When it's a ClassA being initialized, self.__class__ will be "ClassA" , not "LogUserMixin" , so this does exactly what you want. 当正在初始化的ClassAself.__class__将是"ClassA" ,而不是"LogUserMixin" ,所以这正是您想要的。 It works even if your real classes already have base classes, or a hierarchy of subclasses, or if they do additional stuff in __init__ , or take additional arguments; 即使您的真实类已经具有基类或子类的层次结构,或者它们在__init__进行了其他工作或使用了其他参数,该方法仍然有效。 you just need to do a tiny bit more work in some of those cases. 在某些情况下,您只需要多做一点工作即可。

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

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