Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 673
oulenz Avatar asked Nov 04 '17 16:11

oulenz


2 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.

like image 109
Martijn Pieters Avatar answered Sep 18 '22 22:09

Martijn Pieters


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
like image 26
Sam Hartman Avatar answered Sep 17 '22 22:09

Sam Hartman