12

I need to perform certain operations everytime the methods of a particular class is called (for example log the method name). How can this be achieved in Python in a generic way?

vaultah
  • 44,105
  • 12
  • 114
  • 143
Vivek S
  • 5,384
  • 8
  • 51
  • 72
  • You could use [`trace`](https://docs.python.org/2/library/trace.html) or [decorate the methods](http://stackoverflow.com/questions/6200270/decorator-to-print-function-call-details-parameters-names-and-effective-values) – Peter Wood Aug 27 '15 at 09:01
  • 1
    Or just do it in `__getattribute__` – jonrsharpe Aug 27 '15 at 09:02
  • @jonrsharpe It's possible to call `__getattribute__` but not call the function. – Peter Wood Aug 27 '15 at 09:11
  • possible duplicate of [Determine function name from within that function (without using traceback)](http://stackoverflow.com/questions/5067604/determine-function-name-from-within-that-function-without-using-traceback) – Peter Wood Aug 27 '15 at 09:13
  • 2
    @PeterWood that's true, good point; it would log access rather than invocation – jonrsharpe Aug 27 '15 at 09:25

5 Answers5

11

Decorate callable attributes from within a metaclass:

from functools import wraps

def _log_method(val):
    @wraps(val)
    def wrapper(*a, **ka):
        print(val.__name__, 'is called')
        val(*a, **ka)
    return wrapper

class LogMethodCalls(type):
    def __new__(cls, cls_name, bases, attrs):
        for name, attr in attrs.items():
            if callable(attr):
                attrs[name] = _log_method(attr)
        return type.__new__(cls, cls_name, bases, attrs)

class Foo(metaclass=LogMethodCalls):
    def my_method(self):
        pass

Foo().my_method() # my_method is called

Warning: This code only works for instance methods, methods that were decorated with @classmethod or @staticmethod will not be logged (because classmethod and staticmethod objects are not callable - they're just non-data descriptors).


The following works for class methods and static methods as well:

from functools import wraps

def _log_method(val):
    @wraps(val)
    def wrapper(*a, **ka):
        print('calling', val.__name__)
        val(*a, **ka)
    return wrapper

class LogMethodCalls(type):
    def __new__(cls, cls_name, bases, attrs):
        for name, attr in attrs.items():
            if callable(attr):
                attrs[name] = _log_method(attr)
            elif isinstance(attr, (classmethod, staticmethod)):
                attrs[name] = type(attr)(_log_method(attr.__func__))
        return type.__new__(cls, cls_name, bases, attrs)

class Foo(metaclass=LogMethodCalls):

    def my_instance_method(self):
        pass

    @classmethod
    def my_class_method(cls):
        pass

    @staticmethod
    def my_static_method():
        pass

Foo().my_instance_method() # calling my_instance_method
Foo.my_class_method() # calling my_class_method
Foo.my_static_method() # calling my_static_method

They have __func__ attributes that we can decorate.


Note that you'll need to use

class Foo(object):
    __metaclass__ = LogMethodCalls

in Python 2.

vaultah
  • 44,105
  • 12
  • 114
  • 143
  • thanks super useful! edit the `val(*a, **ka)`with `return val(*a, **ka)` to avoid returning **None** from functions that return something different. – M. T. Jan 15 '21 at 10:39
5

Taken from this answer. You can use the inspect module to look at the stack for the function name to create a simple logging function. Seems like kind of a hack, but I suppose it answers the question.

import inspect

def log_call():
    print(inspect.stack()[1][3])

def my_func():
    log_call()
    # do stuff

my_func()

This will print my_func.

Community
  • 1
  • 1
Parker Hoyes
  • 2,118
  • 1
  • 23
  • 38
3

You could implement a decorator:

from functools import wraps

def print_function_name(function):
    @wraps(function)
    def do_it():
        print function.__name__
        function()
    return do_it

Usage:

class MyClass(object):
    @print_function_name
    def some_function(self):
        pass

For example:

>>> my_object = MyClass()
>>> my_object.some_function()
some_function

The use of functools.wraps makes sure the function keeps its documentation and name, instead of becoming do_it.

Peter Wood
  • 23,859
  • 5
  • 60
  • 99
0

Taken from https://stackoverflow.com/a/5103895/5270581:
The following method of object class is called on each access to an attribute of an object, including method calls:

    __get_attribute__

So I suggest to override it by simply adding a call to a logging function inside.
See https://stackoverflow.com/a/5103895/5270581 (go to last answer) for code example.

Community
  • 1
  • 1
Laurent H.
  • 6,316
  • 1
  • 18
  • 40
  • Overrinding `__get_attribute__` is definitely not a good idea when you can do otherwise. – bruno desthuilliers Aug 27 '15 at 10:01
  • @brunodesthuilliers: Can you explain why it is not a good idea ? (or add a link to explanation) – Laurent H. Aug 27 '15 at 19:56
  • The two main reasons why it's not a good idea (when there are other solutions at least) are 1. it's tricky and 2. you'll get a performance penalty on each and every attribute access. In fact `object.__getattribute__` is the default implementation for attribute resolution (which includes handling descriptor protocol and looking up names on class and parents according to the mro), and is implemented in C. So, having the possibility to override it is handy but as with some features in Python it's wisest to only use it when there's no better way. – bruno desthuilliers Aug 28 '15 at 08:22
0

This is my answer from this post here

It can be done many different ways. I will show how to make it through meta-class, class decorator and inheritance.

by changing meta class

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

Also, will show two approaches how to make it without changing meta information of class (through class decorator and class inheritance). The first approach through class decorator put_decorator_on_all_methods accepts decorator to wrap all member callable objects of class.

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

And, recently, I've come across on the same problem, but I couldn't put decorator on class or change it in any other way, except I was allowed to add such behavior through inheritance only (I am not sure that this is the best choice if you can change codebase as you wish though).

Here class Logger forces all callable members of subclasses to write information about their invocations, see code below.

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

Or more abstractly, you can instantiate base class based on some decorator.

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
Alex
  • 3,923
  • 3
  • 25
  • 43