[英]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.