簡體   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