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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

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

Источник: https://habr.com/ru/post/649033/


Интересные статьи

Интересные статьи

Приветствую всех любителей и знатоков языка программирования Python! Сегодня продолжим разбираться с темой анимаций в кроссплатформенном фреймворке для с поддержкой мультитач — Kivy ...
Привет, Хабр! У нас возможен предзаказ долгожданного второго издания книги "Простой Python". Перевод первого издания вышел в 2016 году и по сей день остается в числе бестселл...
7 сентября мы поговорили в прямом эфире с Алексеем Левановым, исполнительным директором в Сбербанке. Леша пришел в Сбербанк в 2014 году на позицию Junior-разработчика. Сейчас он исп...
Около года назад я переквалифицировался из .NET-разработчика в SRE. В этой статье делюсь историей о том, как группа опытных разработчиков отложила в сторону C# и пошла изучать Linux, ...
Холивар. История рунета. Часть 1. Начало: хиппи из Калифорнии, Носик и лихие 90-е Холивар. История рунета. Часть 2. Контркультура: пАдонки, марихуана и Кремль Холивар. История рунета. Часть 3. ...