Python: Apply a decorator to a class instance and static methods
Posted November 9, 2021 by Yaroslav Grebnov ‐ 3 min read
Requirements
Automatically apply a decorator to a class instance and static methods.
Solution idea
Use a meataclass providing the required functionality implementation.
Solution description
The implementation consists of two essential parts:
- implementing the decorator as a classmethod of the metaclass,
- applying the decorator to all the given class methods, including static ones in the metaclass
__new__
method.
In order to make the example more illustrative, we will use the decorator which logs the boundaries of a method execution. Two logging levels will be used, INFO and DEBUG. The actual logging level will be determined based on the log_debug
class attribute value.
Solution part 1. Decorator
As it was mentioned above, the decorator is the metaclass classmethod:
@classmethod
def decorator(mcs, func, log_level):
def wrapper(*args, **kwargs):
log_level(f"Start {func.__func__.__qualname__ if isinstance(func, staticmethod) else func.__qualname__}: "
f"{f', args: {args[1:]}' if len(args) > 1 or kwargs else ''}{f', {kwargs}' if kwargs else ''}")
result = func.__func__(*args, **kwargs) if isinstance(func, staticmethod) else func(*args, **kwargs)
log_level(f"End {func.__func__.__qualname__ if isinstance(func, staticmethod) else func.__qualname__}")
return result
return wrapper
We are logging the method’s __qualname__
and mark the method execution start and end. Static methods we call using func.__func__(*args, **kwargs)
, instance methods - just func(*args, **kwargs)
.
Solution part 2. Metaclass new method
def __new__(mcs, name, bases, attrs):
for attr_name, attr_value in attrs.items():
if isinstance(attr_value, types.FunctionType) or isinstance(attr_value, staticmethod):
attrs[attr_name] = mcs.decorator(
attr_value,
logging.debug if 'log_debug' in attrs and attrs['log_debug'] else logging.info
)
return super().__new__(mcs, name, bases, attrs)
From a given class attributes, we take the instance and static methods:
if isinstance(attr_value, types.FunctionType) or isinstance(attr_value, staticmethod):
The decorator is applied to the selected class attributes. As it was mentioned before, logging level is determined based on the log_debug
class attribute value:
attrs[attr_name] = mcs.decorator(
attr_value,
logging.debug if 'log_debug' in attrs and attrs['log_debug'] else logging.info
)
Usage example
The metaclass can be used in the following way:
- logging at INFO level:
class SomeClass(metaclass=LogMethods)
- logging at DEBUG level:
class SomeOtherClass(metaclass=LogMethods):
log_debug = True
Complete listing
import types
import logging
class LogMethods(type):
"""A metaclass to be used to log class methods' execution boundaries.
Default logging level is INFO. Logging level can be switched to DEBUG by setting
the class log_debug attribute to 'True'."""
def __new__(mcs, name, bases, attrs):
for attr_name, attr_value in attrs.items():
if isinstance(attr_value, types.FunctionType) or isinstance(attr_value, staticmethod):
attrs[attr_name] = mcs.decorator(
attr_value,
logging.debug if 'log_debug' in attrs and attrs['log_debug'] else logging.info
)
return super().__new__(mcs, name, bases, attrs)
@classmethod
def decorator(mcs, func, log_level):
def wrapper(*args, **kwargs):
log_level(f"Start {func.__func__.__qualname__ if isinstance(func, staticmethod) else func.__qualname__}: "
f"{f', args: {args[1:]}' if len(args) > 1 or kwargs else ''}{f', {kwargs}' if kwargs else ''}")
result = func.__func__(*args, **kwargs) if isinstance(func, staticmethod) else func(*args, **kwargs)
log_level(f"End {func.__func__.__qualname__ if isinstance(func, staticmethod) else func.__qualname__}")
return result
return wrapper
In case you have a question or a comment concerning this post, please send them to: