Логировать лучше, чем не логировать. Чем больше разбираешься в чужом и своём коде, тем больше убеждаешься в справедливости этих слов. В 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)
MentalBlood
01.02.2022 16:43+1Имена
debug
,info
и т.д. говорят сами за себя, поэтому присвоим эти методы непосредственно классуКак грубо. Можно было просто добавить классу соответствующий
.logger
— было бы и понятней, и безопаснейyaznahar Автор
01.02.2022 20:26Мы и так добавляем классу атрибут логгера
self.log
и можно использовать его, если так больше нравиться. Или я чего-то не понял?
sophist
02.02.2022 12:35Мы задали значение по умолчанию для первого аргумента
Зачем? В каком случае этот аргумент не будет передан?
yaznahar Автор
02.02.2022 13:08@logged(name="Arthur") class MyClass:
Здесь
logged
сначала вызывается с одним именованным аргументомname
и возвращает декоратор, который уже применяется кMyClass
LazyTalent
Спасибо, вы мне напомнили почему я пользуюсь [loguru](https://github.com/Delgan/loguru)
northzen
Спасибо за ссылку