DEV Community

loading...

Three ways to automatically add functions and methods calling log for Python code

duyixian1234 profile image Yixian Du ・3 min read

The old way, manually logging

import logging
logger = logging.getLogger(__name__)

def func(*args, **kwargs):
    logger.info(f'Call func with {args, kwargs}')

func(1, 2, 3, a=True, b='arg')
INFO:__main__:Call func with ((1, 2, 3), {'a': True, 'b': 'arg'})

1. Use decorators

For Function

def func_logger(func):

    def inner(*args, **kwargs):
        ret = func(*args, **kwargs)
        logger.info(f'Call func {func.__name__} with {args, kwargs} returns {ret}')
        return ret

    return inner


@func_logger
def add(a, b):
    return a + b

add(1,2)
add(1, b=2)
add(a=1, b=2)
INFO:__main__:Call func add with ((1, 2), {}) returns 3
INFO:__main__:Call func add with ((1,), {'b': 2}) returns 3
INFO:__main__:Call func add with ((), {'a': 1, 'b': 2}) returns 3

For Method

We need care of self .

def method_logger(method):

    def inner(self, *args, **kwargs):
        ret = method(self, *args, **kwargs)
        logger.info(f'Call method {method.__name__} of {self} with {args, kwargs} returns {ret}')
        return ret

    return inner

class A:
    a:int
    def __init__(self, a):
        self.a = a

    @method_logger
    def addX(self, x: int):
        return self.a + x

    def __repr__(self):
        return f'A(a={self.a})'

a = A(1)
a.addX(2)
a.addX(x=3)
INFO:__main__:Call method addX of A(a=1) with ((2,), {}) returns 3
INFO:__main__:Call method addX of A(a=1) with ((), {'x': 3}) returns 4

You'd better define custom __str__ or __repr__ method for your class to describe the object in log. One simple way is using dataclass.

from dataclasses import dataclass

@dataclass
class Account:
    uid: str
    banlance: int

    @method_logger
    def transerTo(self, target: 'Account', value:int):
        self.banlance -= value
        target.banlance += value
        return self, target

a = Account('aaaa', 10)
b = Account('bbbb', 10)
a.transerTo(b, 5)
INFO:__main__:Call method transerTo of Account(uid='aaaa', banlance=5) with ((Account(uid='bbbb', banlance=15), 5), {}) returns (Account(uid='aaaa', banlance=5), Account(uid='bbbb', banlance=15))

2. Use __getattribute__ method

def method_logger_x(method, obj):
    def inner(*args, **kwargs):
        ret = method(*args, **kwargs)
        logger.info(f'Call method {method.__name__} of {obj} with {args, kwargs} returns {ret}')
        return ret
    return inner


class MethodLogger:
    def __getattribute__(self, key):
        value = super().__getattribute__(key)
        if callable(value) and not key.startswith('__'):
            return method_logger_x(value, self)
        return value

@dataclass
class Account(MethodLogger):
    uid: str
    banlance: int
    frozen: bool = False

    def transerTo(self, target: 'Account', value:int):
        self.banlance -= value
        target.banlance += value
        return self, target

    def freeze(self, reason:str):
        self.frozen = True

a = Account('aaaa', 10)
b = Account('bbbb', 10)
a.transerTo(b, 5)
a.freeze('Dangerous Action')
INFO:__main__:Call method transerTo of Account(uid='aaaa', banlance=5, frozen=False) with ((Account(uid='bbbb', banlance=15, frozen=False), 5), {}) returns (Account(uid='aaaa', banlance=5, frozen=False), Account(uid='bbbb', banlance=15, frozen=False))
INFO:__main__:Call method freeze of Account(uid='aaaa', banlance=5, frozen=True) with (('Dangerous Action',), {}) returns None

PS When call __getattribute__ method of a object, the value(method) has already get the self parameter.

3. Use meta class

def method_logger(method):
    def inner(self, *args, **kwargs):
        ret = method(self, *args, **kwargs)
        logger.info(f'Call method {method.__name__} of {self} with {args, kwargs} returns {ret}')
        return ret

    return inner

from typing import Tuple
class MethodLoggerMeta(type):
    def __new__(self, name:str, bases:Tuple[type], attrs:dict):
        attrs_copy = attrs.copy()
        for key, value in attrs.items():
            if callable(value) and not key.startswith('__'):
                attrs_copy[key] = method_logger(value)
        return type(name, bases, attrs_copy)

@dataclass
class Account(metaclass=MethodLoggerMeta):
    uid: str
    banlance: int
    frozen: bool = False

    def transerTo(self, target: 'Account', value:int):
        self.banlance -= value
        target.banlance += value
        return self, target

    def freeze(self, reason:str):
        self.frozen = True

a = Account('aaaa', 10)
b = Account('bbbb', 10)
a.transerTo(b, 5)

Conclusion

Adding more logs is usallay a better way. You can choose one of the three approaches you like.

Discussion (0)

Forem Open with the Forem app