Python: наследование и логирование

Не знаю кто как, а я ленивый. Писать длинные строки кода, или повторять один и то-же код много раз в разных местах мне кажется неправильным и нерациональным. Это утверждение в полной мере относится и к записи отладочных сообщений. Если мне нужно организовать правильную запись логов в проекте, многочисленные мануалы рекомендуют использовать примерно такое:


import logging
logger = logging.getLogger()
...
logger.debug(<сообщение уровня DEBUG>)
...

В принципе,- всё ок, если делаем линейный скрипт или классы в одном файле. Можно использовать глобальную переменную, или, на худой конец, определять logger в каждом классе. А если классов,- сто? и в девятнадцати разных модулях? Лениво и неинтересно, давайте попробуем разные способы упростить себе жизнь:

Решение №1 "Логгер-переменная":
инициализировать логгер с помощью logger = logging.getLogger() и использовать во всех местах где хочется:

logger = logging.getLogger()
...
def superfunc(*args):
    global logger
    logger.debug('superfunc!')

Быстро, экономно, но не интересно: зачем вообще заморачиваться с логгингом, если не использовать одну из самых лучших возможностей модуля,- фильтрация логов по источнику? Ведь в этом варианте у нас будет использоваться один и тот-же логгер для всех сообщений, с таким-же успехом можно написать функцию-обёртку над print() для разделения по уровням отладки или использовать print напрямую.

Решение №2 "Логгер как класс":
обернуть логгер и его методы в отдельный класс и использовать объект этого класса везде где нужно.

import logging
class MyLogger(): 
    def __init__(self):
        self.logger = logging.getLogger() 
    def debug(self, msg):
        self.logger.debug(msg)

Ничем не лучше, чем глобальная переменная,- все вызовы записи в лог идут из одних и тех-же методов-обёрток, что сводит возможности фильтрации сообщений по источнику к нулю. Кроме того, это даже хуже, если учитывать промежуточный класс и его экземпляры. А даже если и заморочиться с реализацией синглетона,- не имеет смысла по соображениям, приведённым выше.

Решение №3 "Объектно-ориентированное логирование":
наследовать все классы от волшебного класса LoggingMix, написанного один раз и содержащего обёртки к методам экземпляра класса логирования. При этом логгер можно закопать в сам экземпляр класса-наследника, или даже сделать псевдо-Singleton на уровне корневого класса LoggingMix или классов-наследников. Что-бы не писать лишнего,- обёртки над методами логгера реализуем через лямбда-функции:


class LoggingMix():

    def get_logger(self):
        if not hasattr(self.__class__, '__logger'):
            logger = self.__class__.__logger = logging.getLogger(self.__class__.__module__)
         
        return self.__class__.__logger 
     
    logger = property( get_logger )
    critical = lambda self,msg: self.logger.critical(msg)
    error = lambda self,msg: self.logger.error(msg)
    warning = lambda self,msg: self.logger.warning(msg)
    info = lambda self,msg: self.logger.info(msg)

    debug = lambda self,msg: self.logger.debug(msg)

Тоже плохо: экземпляры дочерних классов становятся "тяжелее" на объект логгера, кроме того, при форматировании отладочных сообщений параметр %(lineno)s продолжает указывать на строку в суперклассе LoggingMix.

Решение №4 "Ныряем в ООП":
Предыдущее решение было почти правильным, но не доведённым до конца. Зачем нам методы-обёртки вызывающие методы экземпляра? Ведь эти методы уже есть у логгера, прикрутим тёплое к мягкому, в смысле используем нужные методы экземпляра логгера прямо в дочернем классе:

class LoggingMix():
    
    def __getattr__(self, name):

        if name in ['critical','error','warning','info','debug']:
            if not hasattr(self.__class__, '__logger'):
                self.__class__.__logger = logging.getLogger(self.__class__.__module__)

            return getattr(self.__class__.__logger, name)


        return super(LoggingMix, self).__getattr__(name)

А вот это уже близко к истине. Наследование от такого класс позволяет использовать логгирование в экземплярах наследников вида self.debug(), кроме того не перекрываются номера строк. Кроме того, можно наследовать от такого класса практически где угодно, даже, например, в моделях или методах Django Class Based View:


from mylogging import LoggingMix
from django.db import models
class ObjectWithLogging(LoggingMix, models.Model): def __init__(self): self.debug('init!') class CreateViewWithLogging(LoggingMix, CreateView): def dispatch(self, *args, **kwargs): self.debug('dispatch started!')