Automatically decorating every instance method in a class

Question:

I want to apply the same decorator to every method in a given class, other than those that start and end with __.

It seems to me it should be doable using a class decorator. Are there any pitfalls to be aware of?

Ideally, I’d also like to be able to:

  1. disable this mechanism for some methods by marking them with a special decorator
  2. enable this mechanism for subclasses as well
  3. enable this mechanism even for methods that are added to this class in runtime

[Note: I’m using Python 3.2, so I’m fine if this relies on features added recently.]

Here’s my attempt:

_methods_to_skip = {}

def apply(decorator):
  def apply_decorator(cls):
    for method_name, method in get_all_instance_methods(cls):
      if (cls, method) in _methods_to_skip:
        continue
      if method_name[:2] == `__` and method_name[-2:] == `__`:
        continue
      cls.method_name = decorator(method)
  return apply_decorator

def dont_decorate(method):
  _methods_to_skip.add((get_class_from_method(method), method))
  return method

Here are things I have problems with:

  • how to implement get_all_instance_methods function
  • not sure if my cls.method_name = decorator(method) line is correct
  • how to do the same to any methods added to a class in runtime
  • how to apply this to subclasses
  • how to implement get_class_from_method
Asked By: max

||

Answers:

I think this is better done with a metaclass, in order to handle both runtime and subclass method decoration. I don’t see an elegant way to handle subclasses automatically with a class decorator.

from types import FunctionType

# check if an object should be decorated
def do_decorate(attr, value):
    return ('__' not in attr and
            isinstance(value, FunctionType) and
            getattr(value, 'decorate', True))

# decorate all instance methods (unless excluded) with the same decorator
def decorate_all(decorator):
    class DecorateAll(type):
        def __new__(cls, name, bases, dct):
            for attr, value in dct.iteritems():
                if do_decorate(attr, value):
                    dct[attr] = decorator(value)
            return super(DecorateAll, cls).__new__(cls, name, bases, dct)
        def __setattr__(self, attr, value):
            if do_decorate(attr, value):
                value = decorator(value)
            super(DecorateAll, self).__setattr__(attr, value)
    return DecorateAll

# decorator to exclude methods
def dont_decorate(f):
    f.decorate = False
    return f

And an example of its use (Python 2, but trivially modified for Python 3):

def printer(f):
    print f
    return f

class Foo(object):
    __metaclass__ = decorate_all(printer)
    def bar(self):
        pass
    @dont_decorate
    def baz(self):
        pass
    @classmethod
    def test(self):
        pass
# prints
# <function bar at 0x04EB59B0>

class AnotherName(Foo):
    def blah(self):
        pass
# prints
# <function blah at 0x04EB5930>

Foo.qux = lambda: 1
# prints
# <function <lambda> at 0x04EB57F0>
Answered By: agf

You could do this (not sure if this is the most elegant way though):

def get_all_instance_methods(x):
    return filter(callable, map(lambda d: getattr(x, d), dir(x)))

As for the cls.method_name, you will have to use getattr(cls, method_name).

Answered By: Gustav Larsson

This is an updated version of @agf’s answer which uses python 3, adds type hints (passing mypy), adds docstrings and does a very minor refactor whereby the decorator does something both before and after the method execution.

from functools import wraps
from types import FunctionType
from typing import Any, Callable, Dict, Tuple


def dont_decorate(f: Callable) -> Callable:
    """Decorator to exclude methods from autodecoration."""
    f._decorate = False  # type: ignore[attr-defined]
    return f


def my_decorator(f: Callable) -> Callable:
    """Dummy decorator which prints before and after the function it decorates."""

    @wraps(f)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        """Wraps provided function and prints before and after."""
        print(f"Calling decorated function {f.__name__}")
        return_val = f(*args, **kwargs)
        print(f"Called decorated function {f.__name__}")
        return return_val

    return wrapper


def decorate_all(decorator: Callable) -> type:
    """Decorate all instance methods (unless excluded) with the same decorator."""

    class DecorateAll(type):
        """Decorate all instance methods (unless excluded) with the same decorator."""

        @classmethod
        def do_decorate(cls, attr: str, value: Any) -> bool:
            """Checks if an object should be decorated."""
            return (
                "__" not in attr
                and isinstance(value, FunctionType)
                and getattr(value, "_decorate", True)
            )

        def __new__(
            cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]
        ) -> type:
            for attr, value in dct.items():
                if cls.do_decorate(attr, value):
                    dct[attr] = decorator(value)
            return super().__new__(cls, name, bases, dct)

        def __setattr__(self, attr: str, value: Any) -> None:
            if self.do_decorate(attr, value):
                value = decorator(value)
            super().__setattr__(attr, value)

    return DecorateAll

And an example of its use:

# Mypy does not support dynamically computed metaclasses.
class Foo(metaclass=decorate_all(decorator=my_decorator)):  # type: ignore[misc]
    """Dummy class."""

    def foo(self) -> None:
        """Should be decorated."""
        print("foo")

    @dont_decorate
    def test(self) -> None:
        """Should not be decorated."""
        print("test")


class Bar(Foo):
    """Dummy class."""

    def bar(self) -> None:
        """Should be decorated."""
        print("bar")



foo = Foo()
foo.foo()
# Calling decorated function foo
# foo
# Called decorated function foo
foo.test()
# test

bar = Bar()
bar.foo()
# Calling decorated function foo
# foo
# Called decorated function foo
bar.bar()
# Calling decorated function bar
# bar
# Called decorated function bar
bar.test()
# test
Answered By: amin_nejad
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.