Search code examples
pythondecoratorclass-members

How to add methods of class to a list inside the class with a decorator


I would like to update a "class-wide" list from a decorator that decorates the class' methods and adds each decorated method to that list.

This is what came to mind:

def add(meth: callable):
    Spam.eggs.append(func)
    return meth

class Spam:
    eggs = []

    @add
    def meth(self):
        pass

This won't work though because Spam hasn't finished defining itself when @add is reached, and thus add raises a NameError, as pointed out in the comments.

I also tried a class method:

class Spam:
    eggs = []

    @classmethod
    def add(cls, meth: callable):
        cls.eggs.append(meth)
        return meth

    @add
    def meth(self):
        pass

But this doesn't work either because when @add is reached, add is bound to the classmethod decorated instance, which is not callable.


Here is what I need this for:

I have a class with several methods that take one argument (besides self) that transform that object in such a way that these methods may be composed with one another. I want to decorate each of these in such a way that they're automatically added to a list in the class.

E.g.:

from typing import List

def transform_meth(meth: callable):
    TextProcessor.transforms.add(meth)
    return meth

class TextProcessor:
    transforms: List[callable] = []

    @transform_meth
    def m1(self, text):
        return text

    @transform_meth
    def m2(self, text):
        return text

    def transform(self, text):
        for transform in self.transforms:
            text = transform(text)
        return text

I could add the methods in the list manually, but I find the decorator to be clearer since it is close to the definition of the method, and thus it is easier to remember to decorate a new method when defining it than adding it to the list manually.


Solution

  • Your current approach fails because when transform_meth is called, TextProcessor isn't bound to anything yet (or if it is, that object gets overwritten when the class statement completes).

    The simple solution would be to define transform_meth inside the class statement, so that it could simply declare transforms as a nonlocal variable. However, that won't work because a class statement doesn't establish a new scope.

    Instead, you can define a function that creates the decorator, which takes the desired list (at that point a just a name in the body of the class statement, not from any assumed scope). That function returns a closure over the list argument so that you can append to it.

    def make_decorator(lst):
    
        # *This* will be the function bound to the name 'transform_meth'
        def _(meth):
            lst.append(meth)
            return meth
    
        return _
    
    
    class TextProcessor:
        transforms: List[callable] = []
    
        transform_meth = make_decorator(transforms)
    
        @transform_meth
        def m1(self, text):
            return text
    
        @transform_meth
        def m2(self, text):
            return text
    
        def transform(self, text):
            for transform in self.transforms:
                text = transform(text)
            return text
    
        del transform_meth  # Not needed anymore, don't create a class attribute