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