Creating a class-level logger

As we noted in Chapter 9, Decorators and Mixins – Cross-Cutting Aspects, creating a class-level logger can be done with a decorator. This will separate logger creation from the rest of the class. A common decorator idea that is very simple has a hidden problem. Here's the example decorator:

def logged(cls: Type) -> Type:
cls.logger = logging.getLogger(cls.__qualname__)
return cls

The @logged decorator creates the logger attribute as a feature of a class. This can then be shared by all of the instances. With this decorator, we can define a class with code like the following example:

@logged
class Player_1:

def __init__(self, bet: str, strategy: str, stake: int) -> None:
self.logger.debug("init bet %s, strategy %s, stake %d", bet,
strategy, stake)

This will assure us that the Player_1 class has the logger with the expected name of logger. We can then use self.logger in the various methods of this class.

The problem with this design is mypy is unable to detect the presence of the logger instance variable. This gap will lead mypy to report potential problems. There are several better approaches to creating loggers.

We can create a class-level debugger using code like the following example:

class Player_2:
logger = logging.getLogger("Player_2")

def __init__(self, bet: str, strategy: str, stake: int) -> None:
self.logger.debug("init bet %s, strategy %s, stake %d", bet, strategy, stake)

This is simple and very clear. It suffers from a small Don't Repeat Yourself (DRY) problem. The class name is repeated within the class-level logger creation. This is a consequence of the way classes are created in Python, and there is no easy way to provide the class name to an object created before the class exists. It's the job of the metaclass to do any finalization of the class definition; this can include providing the class name to internal objects.

We can use the following design to build a consistent logging attribute in a variety of related classes:

class LoggedClassMeta(type):

def __new__(cls, name, bases, namespace, **kwds):
result = type.__new__(cls, name, bases, dict(namespace))
result.logger = logging.getLogger(result.__qualname__)
return result

class LoggedClass(metaclass=LoggedClassMeta):
logger: logging.Logger

This metaclass uses the __new__() method to create the resulting object and add a logger to the class. As an example, a class named C will then have a C.logger object. The LoggedClass can be used as a mixin class to provide the visible logger attribute name and also be sure it's properly initialized.

We'll use this class as shown in the following example:

class Player_3(LoggedClass):

def __init__(self, bet: str, strategy: str, stake: int) -> None:
self.logger.debug(
"init bet %s, strategy %s, stake %d",
bet, strategy, stake)

When we create an instance of Player_3, we're going to exercise the logger attribute. Because this attribute is set by the metaclass for LoggedClass, it is dependably set for every instance of the Player_3 class.

The metaclass and superclass pair is superficially complex-looking. It creates a shared class-level logger for each instance. The name of the class is not repeated in the code. The only obligation on the client is to include LoggedClass as a mixin.

By default, we won't see any output from a definition like this. The initial configuration for the logging module doesn't include a handler or a level that produces any output. We'll also need to change the logging configuration to see any output.

The most important benefit of the way the logging module works is that we can include logging features in our classes and modules without worrying about the overall configuration. The default behavior will be silent and introduce very little overhead. For this reason, we can always include logging features in every class that we define.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset