Как стать автором
Обновить

Логирование в объектах Python. Путь перфекциониста

Время на прочтение3 мин
Количество просмотров13K

Логировать лучше, чем не логировать. Чем больше разбираешься в чужом и своём коде, тем больше убеждаешься в справедливости этих слов. В 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")                        

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

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 12: ↑10 и ↓2+8
Комментарии8

Публикации

Истории

Работа

Python разработчик
119 вакансий
Data Scientist
58 вакансий

Ближайшие события

One day offer от ВСК
Дата16 – 17 мая
Время09:00 – 18:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн
Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область