Лучший способ записывать вызовы методов в Python?
Мы можем закодировать некоторый вид декоратора логирования для эхо-вызовов функций / методов, как показано ниже:
def log(fn):
...
@log
def foo():
...
class Foo(object):
@log
def foo(self):
...
@log
def bar(self, a, b):
...
@log
def foobar(self, x, y, z):
...
Но что, если мы хотим регистрировать вызовы методов, не помещая столько @log перед каждым определением мета? Есть ли способ поместить один декоратор выше определения класса, чтобы все вызовы его методов были оформлены / записаны? Или есть другие, более интересные и интересные способы сделать это вместо декоратора?
4 ответа
См. Присоединение декоратора ко всем функциям в классе
Однако, как указывает принятый ответ на этот вопрос, это, как правило, не очень хорошая идея.
Если вы решите пойти по пути аспектно-ориентированного программирования, я предлагаю начать здесь: Любая библиотека поддержки AOP для Python?
Это может быть излишним, но есть функция трассировки, которая проинформирует вас о большой активности в вашей программе:
import sys
def trace(frame, event, arg):
if event == "call":
filename = frame.f_code.co_filename
if filename == "path/to/myfile.py":
lineno = frame.f_lineno
# Here I'm printing the file and line number,
# but you can examine the frame, locals, etc too.
print "%s @ %s" % (filename, lineno)
return trace
sys.settrace(trace)
call_my_function()
sys.settrace(None)
Это можно сделать разными способами. Я покажу, как это сделать через мета-класс, декоратор классов и наследование.
путем изменения мета - класс
import functools
class Logger(type):
@staticmethod
def _decorator(fun):
@functools.wraps(fun)
def wrapper(*args, **kwargs):
print(fun.__name__, args, kwargs)
return fun(*args, **kwargs)
return wrapper
def __new__(mcs, name, bases, attrs):
for key in attrs.keys():
if callable(attrs[key]):
# if attrs[key] is callable, then we can easily wrap it with decorator
# and substitute in the future attrs
# only for extra clarity (though it is wider type than function)
fun = attrs[key]
attrs[key] = Logger._decorator(fun)
# and then invoke __new__ in type metaclass
return super().__new__(mcs, name, bases, attrs)
class A(metaclass=Logger):
def __init__(self):
self.some_val = "some_val"
def method_first(self, a, b):
print(a, self.some_val)
def another_method(self, c):
print(c)
@staticmethod
def static_method(d):
print(d)
b = A()
# __init__ (<__main__.A object at 0x7f852a52a2b0>,) {}
b.method_first(5, b="Here should be 5")
# method_first (<__main__.A object at 0x7f852a52a2b0>, 5) {'b': 'Here should be 5'}
# 5 some_val
b.method_first(6, b="Here should be 6")
# method_first (<__main__.A object at 0x7f852a52a2b0>, 6) {'b': 'Here should be 6'}
# 6 some_val
b.another_method(7)
# another_method (<__main__.A object at 0x7f852a52a2b0>, 7) {}
# 7
b.static_method(7)
# 7
Кроме того, покажет два подход, как сделать это без изменения метаинформации класса (через класс декоратор и наследование класса). Первый подход через декоратор классов put_decorator_on_all_methods
принимает декоратор для обертывания всех вызываемых объектов класса.
def logger(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
print(f.__name__, args, kwargs)
return f(*args, **kwargs)
return wrapper
def put_decorator_on_all_methods(decorator, cls=None):
if cls is None:
return lambda cls: put_decorator_on_all_methods(decorator, cls)
class Decoratable(cls):
def __init__(self, *args, **kargs):
super().__init__(*args, **kargs)
def __getattribute__(self, item):
value = object.__getattribute__(self, item)
if callable(value):
return decorator(value)
return value
return Decoratable
@put_decorator_on_all_methods(logger)
class A:
def method(self, a, b):
print(a)
def another_method(self, c):
print(c)
@staticmethod
def static_method(d):
print(d)
b = A()
b.method(5, b="Here should be 5")
# >>> method (5,) {'b': 'Here should be 5'}
# >>> 5
b.method(6, b="Here should be 6")
# >>> method (6,) {'b': 'Here should be 6'}
# >>> 6
b.another_method(7)
# >>> another_method (7,) {}
# >>> 7
b.static_method(8)
# >>> static_method (8,) {}
# >>> 8
И недавно я столкнулся с той же проблемой, но я не мог поместить декоратор в класс или изменить его каким-либо другим способом, за исключением того, что мне было разрешено добавлять такое поведение только через наследование (я не уверен, что это лучший выбор, если вы можете изменить кодовую базу по своему желанию).
Вот класс Logger
заставляет все вызываемые члены подклассов записывать информацию об их вызовах, см. код ниже.
class Logger:
def _decorator(self, f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
print(f.__name__, args, kwargs)
return f(*args, **kwargs)
return wrapper
def __getattribute__(self, item):
value = object.__getattribute__(self, item)
if callable(value):
decorator = object.__getattribute__(self, '_decorator')
return decorator(value)
return value
class A(Logger):
def method(self, a, b):
print(a)
def another_method(self, c):
print(c)
@staticmethod
def static_method(d):
print(d)
b = A()
b.method(5, b="Here should be 5")
# >>> method (5,) {'b': 'Here should be 5'}
# >>> 5
b.method(6, b="Here should be 6")
# >>> method (6,) {'b': 'Here should be 6'}
# >>> 6
b.another_method(7)
# >>> another_method (7,) {}
# >>> 7
b.static_method(7)
# >>> static_method (7,) {}
# >>> 7
Или, более абстрактно, вы можете создать экземпляр базового класса на основе некоторого декоратора.
def decorator(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
print(f.__name__, args, kwargs)
return f(*args, **kwargs)
return wrapper
class Decoratable:
def __init__(self, dec):
self._decorator = dec
def __getattribute__(self, item):
value = object.__getattribute__(self, item)
if callable(value):
decorator = object.__getattribute__(self, '_decorator')
return decorator(value)
return value
class A(Decoratable):
def __init__(self, dec):
super().__init__(dec)
def method(self, a, b):
print(a)
def another_method(self, c):
print(c)
@staticmethod
def static_method(d):
print(d)
b = A(decorator)
b.method(5, b="Here should be 5")
# >>> method (5,) {'b': 'Here should be 5'}
# >>> 5
b.method(6, b="Here should be 6")
# >>> method (6,) {'b': 'Here should be 6'}
# >>> 6
b.another_method(7)
# >>> another_method (7,) {}
# >>> 7
b.static_method(7)
# >>> static_method (7,) {}
# >>> 7
Я не уверен, какой у вас вариант использования для этого, но в целом я бы больше подумал о том, что именно является проблемой, которую вы пытаетесь решить.
Тем не менее, вот пример, который может делать то, что вы хотите, но без декоратора:
#!/usr/bin/env python
import inspect
class Foo(object):
def foo(self):
pass
def bar(self, a, b):
pass
def foobar(self, x, y, z):
pass
def __getattribute__(self, name):
returned = object.__getattribute__(self, name)
if inspect.isfunction(returned) or inspect.ismethod(returned):
print 'called ', returned.__name__
return returned
if __name__ == '__main__':
a = Foo()
a.foo()
a.bar(1, 2)
a.foobar(1, 2, 3)
Выход:
called foo
called bar
called foobar
Что ж, если вы не хотите явно декорировать все свои функции, вы можете получить все функции / методы данного модуля и автоматически применить ваш декоратор. не самая простая вещь, но не невозможная в питоне:)
Вы также можете попробовать аспектно-ориентированную среду программирования.
my2c