Логировать лучше, чем не логировать. Чем больше разбираешься в чужом и своём коде, тем больше убеждаешься в справедливости этих слов. В Python есть прекрасный модуль logging: настолько удобный и гибкий, насколько вряд ли когда-нибудь понадобится. Мы не будем обсуждать, как его настроить, благо инструкций для этого хватает. Считаем, что всё уже настроено и надо просто добавить логгер в наши классы, чтобы использовать его внутри объектов:

self.log.info("Hello, world!")

Казалось бы, достаточно написать в конструкторе класса:

import logging

class MyClass:
		def __init__(self):
    		self.log = logging.getLogger("MyClass")   

и всё. Задача решена, статья завершена, спасибо за внимание... именно на этом всё бы и закончилась, если бы я не был перфекционистом. Тиражирование подобных строк в классах противоречит принципу Don't repeat yourself. Конечно, мы можем брать имя логгера из имени класса. В этом случае его достаточно определить в базовом классе, а в потомках он унаследуется уже с правильным именем.

import logging

class BaseClass:
  	def __init__(self):
      	self.log = logging.getLogger(self.__class__.__name__)

Чуть лучше, но всё равно не то. Дублирование осталось, базовых классов может быть много. Стоит попробовать декоратор, чтобы модифицировать класс на этапе создания:

import logging

def logged(cls):
  	cls.log = logging.getLogger(cls.__name__)
    return cls
 
@logged
class MyClass:
  	def __init__(self):
      	self.log.info("Downward is the only way forward")

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

import logging

def logged(cls=None, *, name=""):
		def wrap(cls):
    		cls.log = logging.getLogger(name or cls.__name__)
        return cls
    return wrap if cls is None else wrap(cls)

@logged(name="Arthur")
class MyClass:
  	def __init__(self):
        self.log.info("True inspiration is impossible to fake")

Мы задали значение по умолчанию для первого аргумента, но оставили его позиционным за счёт звёздочки после. Ещё появился именованный аргумент name для именем логгера. Простой декоратор определён внутри параметризованного, поэтому имеет доступ к значению параметра. Чтобы определить, как декоратор вызвали, мы проверяем значение позиционного аргумента и возвращаем либо простой декоратор, либо результат его выполнения.

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

import logging
from functools import wraps

def logged(cls=None, *, name=''):
    def logged_for_init(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            logger_name = name or self.__class__.__name__
            self.log = logging.getLogger(logger_name)
            return func(self, *args, **kwargs)
        return wrapper
    def wrap(cls):
        cls.__init__ = logged_for_init(cls.__init__)
        return cls
    return wrap if cls is None else wrap(cls)

@logged
class MyClass:
    def __init__(self):
      	self.log.info("We need to go deeper")

Конечно, можно ограничиться только декоратором на конструктор и обойтись без классового, но тогда решение получится не таким наглядным и изящным.

В качестве вишенки на торте избавимся от необходимости каждый раз набирать имя атрибута log. Имена debug, info и т.д. говорят сами за себя, поэтому присвоим эти методы непосредственно классу:

import logging
from functools import wraps

def logged(cls=None, *, name=""):
    def logged_for_init(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            logger_name = name or self.__class__.__name__
            self.log = logging.getLogger(logger_name)
            for method_name in ('debug', 'info', 'warning', 'error',
                                'critical', 'exception'):
                method = getattr(self.log, method_name)
                setattr(self, method_name, method)
            return func(self, *args, **kwargs)
        return wrapper
  	def wrap(cls):
        cls.__init__ = logged_for_init(cls.__init__)
        return cls
    return wrap if cls is None else wrap(cls)
  
@logged
class MyClass:
  	def __init__(self):
        self.info("Come back to reality, Dom")                        

Задача решена, статья завершена, спасибо за внимание.

Комментарии (7)


  1. LazyTalent
    01.02.2022 15:19
    +6

    Спасибо, вы мне напомнили почему я пользуюсь [loguru](https://github.com/Delgan/loguru)


    1. northzen
      01.02.2022 17:39

      Спасибо за ссылку


  1. MentalBlood
    01.02.2022 16:43
    +1

    Имена debug, info и т.д. говорят сами за себя, поэтому присвоим эти методы непосредственно классу

    Как грубо. Можно было просто добавить классу соответствующий .logger — было бы и понятней, и безопасней


    1. yaznahar Автор
      01.02.2022 20:26

      Мы и так добавляем классу атрибут логгера self.log и можно использовать его, если так больше нравиться. Или я чего-то не понял?


      1. MentalBlood
        01.02.2022 20:32

        Действительно, пропустил. Я бы на этом остановился )


  1. sophist
    02.02.2022 12:35

    Мы задали значение по умолчанию для первого аргумента

    Зачем? В каком случае этот аргумент не будет передан?


  1. yaznahar Автор
    02.02.2022 13:08

    @logged(name="Arthur")
    class MyClass:

    Здесь logged сначала вызывается с одним именованным аргументом name и возвращает декоратор, который уже применяется к MyClass