一个不错的解决方案是利用自动化机制记录关键函数和方法的调用记录。今天我们来看几种自动记录 Python 函数和方法调用日志的实现手段。
手动记录日志
import logginglogger = 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'})
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 retreturn inner@func_loggerdef add(a, b):return a + badd(1,2)add(1, b=2)add(a=1, b=2)
INFO:__main__:Call func add with ((1, 2), {}) returns 3INFO:__main__:Call func add with ((1,), {'b': 2}) returns 3INFO:__main__:Call func add with ((), {'a': 1, 'b': 2}) returns 3
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 retreturn innerclass A:a:intdef __init__(self, a):self.a = a@method_loggerdef addX(self, x: int):return self.a + xdef __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 3INFO:__main__:Call method addX of A(a=1) with ((), {'x': 3}) returns 4
method_logger装饰器的类最好有定义好
__repr__方法或者
__str__方法,这样可以在日志中直接获取到当前对象的状态。
配合
dataclass使用的话可以省掉自定义模仿方法的操作。
from dataclasses import dataclass@dataclassclass Account:uid: strbanlance: int@method_loggerdef transerTo(self, target: 'Account', value:int):self.banlance -= valuetarget.banlance += valuereturn self, targeta = 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))
使用__getattrbiture__
魔法方法记录方法调用日志
method_logger装饰器,稍微有一点繁琐。解决这个问题也很简单,只需要重写类的
__getattribute__方法。
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 retreturn innerclass MethodLogger:def __getattribute__(self, key):value = super().__getattribute__(key)if callable(value) and not key.startswith('__'):return method_logger_x(value, self)return value@dataclassclass Account(MethodLogger):uid: strbanlance: intfrozen: bool = Falsedef transerTo(self, target: 'Account', value:int):self.banlance -= valuetarget.banlance += valuereturn self, targetdef freeze(self, reason:str):self.frozen = Truea = 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
我们在实现自定义的 __getattribute__
方法的时候判断访问的对象的属性是方法而不是其他字段的时候做了两个判断,一个是当前属性是否可执行(callable(value) is True
),另一个是当前的属性名称不能以双下划线__
开头,不然的话对象实例化调用__init__
方法,打印日志的时候调用__str__
或者__repr__
方法的时候也会记录日志。这里我们没有使用上面的装饰器 mehtod_logger
,而是重新编写一个装饰器函数method_logger_x
。原因是两个装饰器装饰的方法实际上有所区别:method_logger
装饰的方法是在定义类的时候定义的方法,此时self
被认为是一个普通的参数,在装饰器内部调用被装饰方法的时候也要把self
传进去。
而通过__getattribute__
获取到的方法对象本身就是已经实例化好的对象的方法,已经隐含了self
在参数列表中。method_logger_x
需要额外传obj
进去则是为了在记录日志的时候获取到当前方法的宿主对象。
使用元类自动记录方法调用日志
method装饰器在类定义好的时候就已经对需要装饰的方法进行了处理,使用
__attrbiture__魔法方法记录方法调用日志则是在每次调用当前对象的方法时对方法进行了特殊处理,会稍微增加一些运行时开销(对于 Python 解释器并没有什么显著区别)。我们还有另一种方法可以在类定义好的时候就对所有方法增加自动记录调用日志的处理,那就是使用元类机制。
Python 中的元类
type函数现场生成的,函数签名
type(name, bases, dict)中的
name是类的名称,
bases是父类列表(tuple 类型),
dict是类的所有自定义方法和属性的字典。
我们可以用
type函数简单的定义一个类出来。
def A_init(self, a:int):self.a = aA = type('A',(object,),dict(__init__=A_init, __str__= lambda self: f'A(a={self.a})'))a = A(1)print(a)# A(a=1)
Meta继承
type,并且重写
__new__方法,我们就定义了一个元类。
from typing import Tuplefrom datetime import datetimeclass Meta(type):def __new__(self, name:str, bases:Tuple[type], attrs:dict):attrs['time_defined'] = datetime.now()return type(name, bases, attrs)class A(metaclass=Meta):passA.time_defined# datetime.datetime(2020, 9, 6, 14, 14, 7, 938370)
Meta为元类的类会自动添加一个属性
time_defined, 得到类在 Python 解释器的实际定义时间。
type类型重写
__new__方法在指定了元类的类定义的时候改变其行为的一种机制。
使用元类自动记录方法调用日志
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 retreturn innerfrom typing import Tupleclass 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)@dataclassclass Account(metaclass=MethodLoggerMeta):uid: strbanlance: intfrozen: bool = Falsedef transerTo(self, target: 'Account', value:int):self.banlance -= valuetarget.banlance += valuereturn self, targetdef freeze(self, reason:str):self.frozen = Truea = Account('aaaa', 10)b = Account('bbbb', 10)a.transerTo(b, 5)
self参数的
method_logger版本,原因和上述说明一致,在定义的时候类的方法就是普通的函数,没有隐式的 self 对象。
总结
文章转载自追不上乌龟的兔子,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




