轉載請注明:
仰望高端玩家的小清新 http://www.cnblogs.com/luruiyuan/
通常我們在構建 python 系統時,往往需要一個簡單的 logging 框架。python 自帶的 logging 框架的確十分完善,但是本身過於復雜,因此需要自行封裝來滿足我們的高(zhuang)端(b)需求
1. 常用的格式化字符串:
這是我比較常用的格式化字符串,不同的人可能有不同的習慣
1 # 第一種,月日年的輸出 2 DEFAULT_DATE_FMT = '%a, %p %b %d %Y %H:%M:%S' 3 # Wed, Sep 27 2017 18:56:40 4 5 #第二種,年月日 6 DEFAULT_DATE_FMT = '%Y-%m-%d %a, %p %H:%M:%S' 7 # Wed, 2017-09-27 18:59:33
2. logging 框架的簡單基本用法:
1 # 簡單的logging配置 2 import logging 3 4 logging.basicConfig(level=logging.DEBUG, 5 format='[%(asctime)s %(filename)s [line:%(lineno)d]] %(levelname)s %(message)s', 6 datefmt='%a, %d %b %Y %H:%M:%S', 7 filename='myapp.log', 8 filemode='w')
這樣的好處是,在一些情況下可以簡單配置log之后輸出,但是其格式中的樣式是難以變化的
3. 封裝自己的 logger 框架
毫無疑問,為了方便代碼的維護和重構,職責單一原則必不可少。目前的 v0.1 版本的 UML 圖如下:
3.1 顏色:
CmdColor 類主要用於存儲命令行控制台的字體轉義字符串,並且保證顏色名稱到顏色轉義字符串的映射,其中包括一些常用的顏色
其中代碼如下:
本類作為顏色的映射,主要實現了獲取所有顏色,以及查重的set,以及名稱到字符串的映射

1 class CmdColor(): 2 ''' Cmd color escape strings ''' 3 # color escape strings 4 __COLOR_RED = '\033[1;31m' 5 __COLOR_GREEN = '\033[1;32m' 6 __COLOR_YELLOW = '\033[1;33m' 7 __COLOR_BLUE = '\033[1;34m' 8 __COLOR_PURPLE = '\033[1;35m' 9 __COLOR_CYAN = '\033[1;36m' 10 __COLOR_GRAY = '\033[1;37m' 11 __COLOR_WHITE = '\033[1;38m' 12 __COLOR_RESET = '\033[1;0m' 13 14 # color names to escape strings 15 __COLOR_2_STR = { 16 'red' : __COLOR_RED, 17 'green' : __COLOR_GREEN, 18 'yellow': __COLOR_YELLOW, 19 'blue' : __COLOR_BLUE, 20 'purple': __COLOR_PURPLE, 21 'cyan' : __COLOR_CYAN, 22 'gray' : __COLOR_GRAY, 23 'white' : __COLOR_WHITE, 24 'reset' : __COLOR_RESET, 25 } 26 27 __COLORS = __COLOR_2_STR.keys() 28 __COLOR_SET = set(__COLORS) 29 30 @classmethod 31 def get_color_by_str(cls, color_str): 32 if not isinstance(color_str, str): 33 raise TypeError("color string must str, but type: '%s' passed in." % type(color_str)) 34 color = color_str.lower() 35 if color not in cls.__COLOR_SET: 36 raise ValueError("no such color: '%s'" % color) 37 return cls.__COLOR_2_STR[color] 38 39 @classmethod 40 def get_all_colors(cls): 41 ''' return a list that contains all the color names ''' 42 return cls.__COLORS 43 44 @classmethod 45 def get_color_set(cls): 46 ''' return a set contains the name of all the colors''' 47 return cls.__COLOR_SET
后續可以做的擴展:顏色可以作為單獨的抽象類,各個平台的顏色,如 CmdColor 作為其子類實現具體的顏色方法,這樣可以增強健壯性和可擴展性
由於 win 平台和 *nix 平台對於輸出處理不同,因此在目前的版本中,如果在win平台調用,則直接禁用了顏色的輸出。
3.2 logging 的格式:
同樣,為了保證 logging 打印的數據格式一致,通過 BasicFormatter 類將 logging 模塊的元數據處理為一致的格式,可以保證在彩色和黑白的情況下數據的格式一致性,更重要的是這一抽象也保證了這一格式在日后被其他 handler 復用時的格式一致性。
其中的 format 和 formatTime 方法覆蓋了父類 logging.Formatter 中的同名方法,這樣通過繼承機制很好的模擬了多態,這樣我們的公用格式就可以得到復用
3.2.1 修正無法顯示毫秒的問題
這里還有一個細節需要注意:
在 logging.Formatter 中的 formatTime 在沒有傳入時間格式字符串時需要的是會顯示毫秒,但是一旦傳遞了該參數,就無法精確到秒以下的單位。這是由於 logging.Formatter 直接使用了 time.strftime 函數來格式化時間,而該函數參照了 ISO8601 標准,這一標准並未規定比秒更小的時間單位該如何表示,問題由此產生。
但是,注意到在默認不傳參情況下 formatTime 會顯示毫秒,因此我們只需要知道這里毫秒數是如何產生的即可
logging.Formatter.formatTime 的關鍵代碼如下:
1 ct = self.converter(record.created) 2 if datefmt: 3 s = time.strftime(datefmt, ct) 4 else: 5 t = time.strftime(self.default_time_format, ct) 6 s = self.default_msec_format % (t, record.msecs) 7 return s
我們不難發現,最關鍵的部分是 record.msecs,因此我們可以知道,我們只需要通過該參數,即可獲得秒以下的時間單位。通過測試,我發現這是一個小數,既然如此,剩下的就不用我說了吧~
綜上,我們可以得到該類的主要代碼:

1 class BasicFormatter(Formatter): 2 3 def __init__(self, fmt=None, datefmt=None): 4 super(BasicFormatter, self).__init__(fmt, datefmt) 5 self.default_level_fmt = '[%(levelname)s]' 6 7 def formatTime(self, record, datefmt=None): 8 ''' @override logging.Formatter.formatTime 9 default case: microseconds is added 10 otherwise: add microseconds mannually''' 11 asctime = Formatter.formatTime(self, record, datefmt=datefmt) 12 return asctime if datefmt is None or datefmt == '' else self.default_msec_format % (asctime, record.msecs) 13 14 def format(self, record): 15 ''' @override logging.Formatter.format 16 generate a consistent format''' 17 msg = Formatter.format(self, record) 18 pos1 = self._fmt.find(self.default_level_fmt) # return -1 if not find 19 pos2 = pos1 + len(self.default_level_fmt) 20 if pos1 > -1: 21 last_ch = self.default_level_fmt[-1] 22 repeat = self._get_repeat_times(msg, last_ch, 0, pos2) 23 pos1 = self._get_index(msg, last_ch, repeat) 24 return '%-10s%s' % (msg[:pos1], msg[pos1+1:]) 25 else: 26 return msg
3.3 具體的 CmdColoredFormatter 格式類:
這個類已經不再是抽象了,而是在 BasicFormatter 的基礎上對 logging 中的信息進一步美化——上色的過程
這個類只負責上色,不涉及 logging 中的時間處理,因此我們只需覆蓋 format 方法即可,顏色的處理已經主要聚合在 CmdColor 類中,因此本類較為簡單
本類的代碼如下:

1 class CmdColoredFormatter(BasicFormatter): 2 ''' Cmd Colored Formatter Class''' 3 4 # levels list and set 5 __LEVELS = ['NOTSET', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] 6 __LEVEL_SET = set(__LEVELS) 7 8 def __init__(self, fmt=None, datefmt=None, **level_colors): 9 super(CmdColoredFormatter, self).__init__(fmt, datefmt) 10 self.LOG_COLORS = {} # a dict, used to convert log level to color 11 self.init_log_colors() 12 self.set_level_colors(**level_colors) 13 14 def init_log_colors(self): 15 ''' initialize log config ''' 16 for lev in CmdColoredFormatter.__LEVELS: 17 self.LOG_COLORS[lev] = '%s' 18 19 def set_level_colors(self, **kwargs): 20 ''' set each level different colors ''' 21 lev_set = CmdColoredFormatter.__LEVEL_SET 22 color_set = CmdColor.get_color_set() 23 24 # check log level and set colors 25 for lev, color in kwargs.items(): 26 lev, color = lev.upper(), color.lower() 27 if lev not in lev_set: 28 raise KeyError("log level '%s' does not exist" % lev) 29 if color not in color_set: 30 raise ValueError("log color '%s' does not exist" % color) 31 self.LOG_COLORS[lev] = ''.join([CmdColor.get_color_by_str(color), '%s', CmdColor.get_color_by_str('reset')]) 32 33 def format(self, record): 34 ''' @override BasicFormatter.format''' 35 msg = super(CmdColoredFormatter, self).format(record) 36 # msg = BasicFormatter.format(self, record) # 本行和上一行等價 37 return self.LOG_COLORS.get(record.levelname, '%s') % msg
3.4 Logger 類:
通過前面各個類的准備工作,Logger 類就可以初具雛形了。
1. 幾個參數的相關解釋:
1. 參數列表: __LOG_ARGS
2. 初始化:除了固定的幾個參數,其余參數的初始化通過 kwargs 傳入的 dict 在 set_logger 方法中動態初始化
這里有一些小 trick 可以簡化我們的代碼,並且具有良好的可擴展新
1 # 在某個函數定義內調用,可獲得函數的所有參數,以 dict 為形式 2 # 每次調用時返回一個新的 dict,注意,參數 self 或者 cls 也會包含在內 3 # 需要用 pop() 方法去除 4 arg_dict = locals() 5 6 # 獲取對象中某個屬性或方法,不存在時返回 default 中的內容 7 getattr(obj, name, default=None) 8 # 動態設置對象中的屬性值或者函數指針 9 setattr(obj, name, value)
3. 添加 handler:
目前還沒有用到更復雜的 http 和 socket 的 handler , 因此這里暫時沒有封裝相應的方法,后續可以封裝成一個簡單工廠,等用到再說。
目前只用到了 fileHandler 和 streamHandler ,因此只能輸出到控制台以及文件。

1 def __add_filehandler(self): 2 ''' Add a file handler to logger ''' 3 # Filehandler 4 if self.backup_count == 0: 5 self.filehandler = logging.FileHandler(self.filename, self.filemode) 6 # RotatingFileHandler 7 elif self.when is None: 8 self.filehandler = logging.handlers.RotatingFileHandler(self.filename, 9 self.filemode, self.limit, self.backup_count) 10 # TimedRotatingFileHandler 11 else: 12 self.filehandler = logging.handlers.TimedRotatingFileHandler(self.filename, 13 self.when, 1, self.backup_count) 14 15 formatter = BasicFormatter(self.filefmt, self.filedatefmt) 16 self.filehandler.setFormatter(formatter) 17 self.logger.addHandler(self.filehandler) 18 19 def __add_streamhandler(self): 20 ''' Add a stream handler to logger ''' 21 self.streamhandler = logging.StreamHandler() 22 self.streamhandler.setLevel(self.cmdlevel) 23 formatter = CmdColoredFormatter(self.cmdfmt, self.cmddatefmt, 24 **self.cmd_color_dict) if self.colorful else BasicFormatter(self.cmdfmt, self.cmddatefmt) 25 self.streamhandler.setFormatter(formatter) 26 self.logger.addHandler(self.streamhandler)
4. 基於 loggername 的單例模式:
使用過 logging 的都知道,相同的 loggername 獲取的 logging 模塊的實例是相同的,因此自行封裝的 logger 框架也應該遵循類似的模式,即基於 loggername 的類單例模式。
這里只需要注意 3 點:1. 線程並發安全性——加鎖 2. loggername 到相應 instance 的映射 3. Logger 類本身允許多例,但是同一個 loggername 只允許單例
但是要注意,__init__ 本身只能返回 None ,因而拿不到對象引用,每個類在創建實例的時候,實際上是由類調用了 __new__ 方法返回對象引用,這個引用再作為 self 參數傳入 __init__ 中初始化該對象,因此實現中的 __new__ 是一個容易忽略的細節。
相應實現如下:

1 @classmethod 2 def get_logger(cls, **kwargs): 3 loggername = kwargs['loggername'] 4 cls.__lock.acquire() # lock current thread 5 if loggername in cls.__name2logger: 6 cls.__name2logger[loggername].set_logger(**kwargs) 7 else: 8 log_obj = object.__new__(cls) 9 cls.__init__(log_obj, **kwargs) 10 cls.__name2logger[loggername] = log_obj 11 cls.__lock.release() # release lock 12 return cls.__name2logger[loggername]
5. set_logger: 通過一個方法設置所有的相關參數
這里體現出了 setattr 的用處,通過這樣的方法能夠動態的添加 / 修改相關的對象屬性
通過對象的屬性重新加載
其實現如下:

1 def set_logger(self, **kwargs): 2 ''' Configure logger with dict settings ''' 3 for k, v in kwargs.items(): 4 if k not in Logger.__log_arg_set: 5 raise KeyError("config argument '%s' does not exist" % k) 6 setattr(self, k, v) # add instance attributes 7 8 if self.cmd_color_dict is None: 9 self.cmd_color_dict = {'debug': 'green', 'warning':'yellow', 'error':'red', 'critical':'purple'} 10 if isinstance(self.cmdlevel, str): 11 self.cmdlevel = getattr(logging, self.cmdlevel.upper(), logging.DEBUG) 12 if isinstance(self.filelevel, str): 13 self.filelevel = getattr(logging, self.filelevel.upper(), logging.INFO) 14 15 self.__init_logger() 16 self.__import_log_func() 17 if self.cmdlog: 18 self.__add_streamhandler() 19 if self.filelog: 20 self.__add_filehandler()
6. 其他:
在實現基於 loggername 的單例模式時,有一些基於反射的想法,雖然失敗了,但是也是對反射方式的一種嘗試
以下這個裝飾器就是我第一次時試圖加在 __init__ 上的裝飾器,但是由於 __init__ 強制返回 None 而無法拿到對象引用而失敗,但是實際上如果用在 __new__ 上即可。
這里展示了從函數外通過反射獲取傳入函數參數的方法:
與 locals() 對應,inspect.signature(func_name).parameters 可以從函數外通過反射的方式獲取到傳入函數的參數和值,返回值為:
OrdereDict,例如一個函數 func(a,b),調用為 func(1, 2)
則返回一個 OrdereDict {'a': 'a=1', b: 'b=2'}
相應的實現如下:
1 import inspect 2 3 # 基於 loggername 的單例裝飾器 4 def singletonLoggerByName(cls): 5 __name2logger = {} 6 def getValueByArg(orderedDict, arg): 7 return str(orderedDict[arg]).partition('=')[-1] 8 9 def wrapper(self, logger_init, **kwargs): 10 default_values = inspect.signature(logger_init).parameters 11 name = kwargs.get('loggername', getValueByArg(default_values, 'loggername')) 12 print('name not in __name2logger: %r' % (name not in __name2logger)) 13 if name not in __name2logger: 14 logger_init(self, **kwargs) 15 __name2logger[name] = self 16 print(__name2logger[name]) 17 return __name2logger[name] # 裝飾器用於 __init__ 是不行的,因為 python 中 __init__ 只能返回 None, 這樣單例模式中后續的引用無法綁定到第一次的實例上 18 return wrapper
7.效果圖:
完整代碼詳見:log/logger.py
參考資料:大佬的博客
今天就到這里啦~lalala