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

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

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

Читайте так же:

  • Microsoft переложила закрытие уязвимости OMIGOD в Azure на пользователейMicrosoft переложила закрытие уязвимости OMIGOD в Azure на пользователей Пользователи Microsoft Azure, использующие виртуальные машины Linux в облаке, должны принять меры, чтобы защитить себя от уязвимостей OMIGOD внутри платформы Open Management Infrastructure (OMI).Microsoft опубликовала патчи 14 сентября. Наименее серьезная из четырех уязвимостей имеет […]
  • Задания и customTask в Google Analytics Многие специалисты обходят стороной вопросы того, как изнутри работают инструменты, с которыми они взаимодействуют. Иногда даже незначительное погружение «под капот» может дать массу информации, которая поможет лучше понимать и эффективнее работать с инструментом. Например, в […]
  • Из-за давления властей Китая на отрасль местные IT-компании начали сокращать персоналИз-за давления властей Китая на отрасль местные IT-компании начали сокращать персонал Китайские IT-компании прибегли к сокращениям сотрудников на фоне давления местных властей на технологический сектор. Так, например, видосервис iQiyi Inc. планирует уволить сотни сотрудников из некоторых департаментов — это около 20% всего персонала, сообщает […]
  • Издатели пожаловались на Facebook в РоскомнадзорИздатели пожаловались на Facebook в Роскомнадзор Ассоциация по защите авторских прав в интернете (АЗАПИ) пожаловалась Роскомнадзору на Facebook и попросила привлечь компанию к административной ответственности.  Поводом для обращения издателей к регулятору стало массовое пиратство книг в принадлежащей Facebook соцсети Instagram и […]