How to make new decorators available within a class without explicitly importing them?

Viewed 654

Is it possible to modify a class so as to make available a certain method decorator, without having to explicitly import it and without having to prefix it (@something.some_decorator):

class SomeClass:

    @some_decorator
    def some_method(self):
        pass

I don't think this is possible with a class decorator, because that is applied too late. The option that seems more promising is using a metaclass, but I am unsure how, my guess is that I would have to have some_decorator be introduced into the namespace of SomeClass.

Thanks to @MartijnPieters for pointing out that staticmethod and classmethod are built-ins. I had expected them to be part of the type machinery.

To be clear, I don't have any explicit use-case for this, I am just curious as to whether this is at all possible.

ADDENDUM, now that the question has been answered. The original reason why I was looking beyond simply importing or defining a decorator locally, was that I had defined a decorator that would only work if a certain container attribute was initialised on an object, and I was looking for a way to tie enforcing this to the availability of the decorator. I ended up checking whether the attribute existed and if not initialising it within the decorator, which may very well be the lesser evil here.

3 Answers

Yes, in Python 3 you can use the metaclass __prepare__ hook. It is expected to return a mapping, and it forms the basis of the local namespace for the class body:

def some_decorator(f):
    print(f'Decorating {f.__name__}')
    return f

class meta(type):
    @classmethod
    def __prepare__(mcls, name, bases, **kw):
        return {'some_decorator': some_decorator}

class SomeClass(metaclass=meta):
    @some_decorator
    def some_method(self):
        pass

Running the above produces

Decorating some_method

However, you should not use this. As the Zen of Python states: Explicit is better than implicit, and introducing magic names into your classes can easily lead to confusion and bugs. Importing a metaclass is no different than importing a decorator, you replaced one name with another.

A class decorator can still apply other decorators to methods on a class after the class body has been created. The @decorator syntax is just syntactic sugar for name = decorator(decorated_object), you can always apply a decorator later on by using name = decorator(name), or in a class context, as cls.name = decorator(cls.name). If you need to pick and choose which methods this should apply to, you could pick criteria like the method name, or attributes set on the method, or the docstring of the method, etc. Or just use the decorator directly on the methods.

As best I can tell a metaclass can do this. You somehow need to get the decorators available to the metaclass, possibly by importing, and then you can include them in the prepared namespace:

class includewraps(type):
    def prepare(*args):
        from functools import wraps
        return {'wraps': wraps}


class haswraps (metaclass = includewraps):
    # wraps is available in this scope

Write a decorator that takes a list of strings and imports them before the function is called for the first time. This avoids explicit imports until the last possible moment before they're needed. This, like all the other answers here, is probably an indication that your code should be restructured instead.

from functools import wraps
from importlib import import_module

def deferred(*names):
    def decorator(f):
        # this will hold the fully decorated function
        final_f = None

        @wraps(f)
        def wrapper(*args, **kwargs):
            nonlocal final_f

            if final_f is None:
                # start with the initial function
                final_f = f

                for name in names:
                    # assume the last . is the object to import
                    # import the module then get the object
                    mod, obj = name.rsplit('.', 1)
                    d = getattr(import_module(mod), obj)
                    # decorate the function and keep going
                    final_f = d(final_f)

            return final_f(*args, **kwargs)

        return wrapper

    return decorator
# for demonstration purposes, decorate with a function defined after this
# assumes this file is called "example.py"
@deferred('example.double')
def add(x, y):
    return x + y

def double(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        return 2 * f(*args, **kwargs)

    return wrapper

if __name__ == '__main__':
    print(add(3, 6))  # 18

The arguments to deferred should be strings of the form 'path.to.module.decorator. path.to.module is imported and then decorator is retrieved from the module. Each decorator is applied to wrap the function. The function is stored in a nonlocal so that this import and decorating only needs to happen the first time the function is called.

Related