6

I want to log every method call in some classes. I could have done

class Class1(object):
    @log
    def method1(self, *args):
        ...
    @log
    def method2(self, *args):
        ...

But I have a lot of methods in every class, and I don't want to decorate every one separately. Currently, I tried using a hack with metaclasses (overriding my logged class' __getattribute__ so that if I try to get a method, it'll return a logging method instead):

class LoggedMeta(type):
    def __new__(cls, name, bases, attrs):
        def __getattribute__(self, name_):
            attr = super().__getattribute__(name_)
            if isinstance(attr, (types.MethodType, types.FunctionType)) and not name_.startswith("__"):
                return makeLogged(attr) #This returns a method that first logs the method call, and then calls the original method.
            return attr
        attrs["__getattribute__"] = __getattribute__
    return type.__new__(cls, name, bases, attrs)

class Class1(object):
    __metaclass__ = LoggedMeta
    def method1(self, *args):
        ...

However, I'm on Python 2.X, and the super() syntax doesn't work. At the time I call super, I don't have the __getattribute__'s class (but I do have its class name), so I can't use the old super syntax super(Class, Inst).

I tried earlier to use metaclasses, but override all the methods instead of __getattribute__, but I want to log static method calls also, and they gave me some trouble.

I searched for this type of question, but found no-one who tried changing a class this way.

Any ideas or help would be very appreciated.

EDIT: My solution was this (mostly taken from this thread):

import inspect, types

CLASS = 0
NORMAL = 1
STATIC = 2

class DecoratedMethod(object):

    def __init__(self, func, type_):
        self.func = func
        self.type = type_

    def __get__(self, obj, cls=None):
        def wrapper(*args, **kwargs):
            print "before"
            if self.type == CLASS:
                #classmethods (unlike normal methods) reach this stage as bound methods, but args still contains the class
                #as a first argument, so we omit it.
                ret = self.func(*(args[1:]), **kwargs)
            else:
                ret = self.func(*args, **kwargs)
            print "after"
            return ret
        for attr in "__module__", "__name__", "__doc__":
            setattr(wrapper, attr, getattr(self.func, attr))
        if self.type == CLASS:
            return types.MethodType(wrapper, cls, type)
        elif self.type == NORMAL:
            return types.MethodType(wrapper, obj, cls) 
        else:
            return wrapper

def decorate_class(cls):
    for name, meth in inspect.getmembers(cls):
        if inspect.ismethod(meth):
            if inspect.isclass(meth.im_self):
                # meth is a classmethod
                setattr(cls, name, DecoratedMethod(meth, CLASS))
            else:
                # meth is a regular method
                setattr(cls, name, DecoratedMethod(meth, NORMAL))
        elif inspect.isfunction(meth):
            # meth is a staticmethod
            setattr(cls, name, DecoratedMethod(meth, STATIC))
    return cls


@decorate_class
class MyClass(object):

    def __init__(self):
        self.a = 10
        print "__init__"

    def foo(self):
        print self.a

    @staticmethod
    def baz():
        print "baz"

    @classmethod
    def bar(cls):
        print "bar"

later I cleaned it up a bit, but that's the solution's essence. I need this difference between class, static and normal methods because I want to have

inst = MyClass()
assert type(inst.baz) == types.FunctionType
assert type(inst.foo) == types.MethodType
assert type(inst.bar) == types.MethodType
Community
  • 1
  • 1
WallE
  • 63
  • 1
  • 5
  • Possible duplicate of [How can I decorate all functions of a class without typing it over and over for each method added? Python](http://stackoverflow.com/questions/6307761/how-can-i-decorate-all-functions-of-a-class-without-typing-it-over-and-over-for) – Flynsee Nov 29 '16 at 01:21

4 Answers4

11

Why don't you alter the class object?

You can go through the methods in a class with dir(MyClass) and replace them with a wrapped version... something like:

def logify(klass):
    for member in dir(klass):
        if not callable(getattr(klass, method))
            continue # skip attributes
        setattr(klass, method, log(method))

tinker around with something like this... should work...

Lesmana
  • 25,663
  • 9
  • 82
  • 87
Daren Thomas
  • 67,947
  • 40
  • 154
  • 200
3

A class decorator can help here. Decorate the whole class and add you logging functionality to all callable attributes the class have.

Lesmana
  • 25,663
  • 9
  • 82
  • 87
Mike Müller
  • 82,630
  • 20
  • 166
  • 161
1

I suggest taking for_all_methods decorator from this SO post, then your code would be

@for_all_methods(log)
class Class1():
   def method1(self): pass
   ...
Community
  • 1
  • 1
Iwan LD
  • 322
  • 4
  • 13
1

If the goal is to make your code easier to debug by simply logging call and response, check out the Autologging module. A single annotation is all it takes =)

https://pythonhosted.org/Autologging/examples-traced.html

pip install Autologging

.

# my_module.py

from autologging import traced


@traced
class MyClass:

   def __init__(self):
      self._value = "ham"

   def my_method(self, arg, keyword=None):
      return "%s, %s, and %s" % (arg, self._value, keyword)

.

>>> import logging, sys
>>> from autologging import TRACE
>>> logging.basicConfig(level=TRACE, stream=sys.stdout,
...     format="%(levelname)s:%(name)s:%(funcName)s:%(message)s")
>>> from my_module import MyClass
>>> my_obj = MyClass()
TRACE:my_module.MyClass:__init__:CALL *() **{}
TRACE:my_module.MyClass:__init__:RETURN None
>>> my_obj.my_method("spam", keyword="eggs")
TRACE:my_module.MyClass:my_method:CALL *('spam',) **{'keyword': 'eggs'}
TRACE:my_module.MyClass:my_method:RETURN 'spam, ham, and eggs'
'spam, ham, and eggs'
curious_prism
  • 349
  • 1
  • 5
  • TBH, I really can't remember what the goal was, but this looks like it exactly soldes the question. – WallE Jun 18 '19 at 06:45