zekel
zekel

Reputation: 9467

How do you "mark" a group of Python methods for later use? (Can these decorators be improved?)

I'd like to mark some methods (class methods in this case, but not necessarily) as related so I can use them together. I don't want to create a hardcoded list of the methods inside all_fruit because I don't want it to be missed when adding a new type of fruit. Ideally, I'd be able to do something like this:

class Food:
    @classmethod
    @fruitdecorator
    def apples(clss, amount):
        return '%d apple(s)' % amount

    @classmethod
    @fruitdecorator
    def oranges(clss, amount):
        return '%d orange(s)' % amount

    @classmethod
    def carrots(clss, amount):
        return '%d carrot(s)' % amount

    @classmethod
    def all_fruit(clss, amount):
        pass # somehow return all methods decorated by @fruitdecorator

for method in Food.all_fruit():
    print method(Food, 2)
    # '2 apples'
    # '2 oranges'

I found these decorator answers which let me do the following, which is OK, but I was wondering if it could be simplified at all. In this case I have to define this stuff and call it before, after, and use it in the fruit_baseket class method, which seems a little tedious. (I'm open to non-decorator solutions but they seem like the best way to keep the "mark" and the function definition close together.)

def MakeDecorator():
    registry = []
    def decorator(fn):
        # print dir(fn)
        registry.append(fn)
        return fn
    decorator.all = registry
    return decorator

fruitdecorator = MakeDecorator()

class Food:
    @classmethod
    @fruitdecorator
    def apples(clss, amount):
        return '%d apple(s)' % amount

    @classmethod
    @fruitdecorator
    def oranges(clss, amount):
        return '%d orange(s)' % amount

    @classmethod
    def carrots(clss, amount):
        return '%d carrot(s)' % amount

    @classmethod
    def fruit_basket(clss, amount):
        basket = [xx(clss, amount) for xx in clss.fruit_methods]
        return basket


Food.fruit_methods = fruitdecorator.all
print Food.fruit_basket(2)

Upvotes: 3

Views: 1545

Answers (2)

iafisher
iafisher

Reputation: 1008

It's possible to do this in a more clean, if somewhat more complicated way, using metaclasses. If you have your decorator flag its function in someway (for example, by adding a special attribute), then the metaclass can create a list as a class variable and populate it with the decorated functions (it can tell which have been decorated by looking for the special attribute). A sample implementation would be

class FoodMeta(type):
    def __new__(meta, name, bases, dct):
        fruit_methods = []
        for key, value in dct.items():
            if hasattr(value, 'fruity'):
                fruit_methods.append(value)
        dct['fruit_methods'] = fruit_methods
        return super(FoodMeta, meta).__new__(meta, name, bases, dct)

def fruitdecorator(f):
    def inner(*args, **kwargs):
        # do whatever
        pass
    inner.fruity = True
    return inner

class Food(object):
    __metaclass__ = FoodMeta

    @fruitdecorator
    @classmethod
    def apples(clss, amount):
        return '%d apple(s)' % amount

    @fruitdecorator
    @classmethod
    def oranges(clss, amount):
        return '%d orange(s)' % amount

    @classmethod
    def carrots(clss, amount):
        return '%d carrot(s)' % amount

    @classmethod
    def fruit_basket(clss, amount):
        basket = [xx(clss, amount) for xx in clss.fruit_methods]
        return basket

Food.fruit_methods # [Food.apples, Food.oranges]

A crucial thing to note in this solution is that the order of decorators matters: fruitdecorator must go on the outside (i.e., apply after) classmethod. This is because the classmethod decorator doesn't preserve the fruity attribute on the method, so the metaclass wouldn't know it was registered in the first place.

Upvotes: 1

aghast
aghast

Reputation: 15310

A decorator takes a function and returns a function. I think your solution is maybe too general, unless you see a need for vegetabledecorator and saladdecorator markers, also.

Fruit_methods = []

def fruit(f):
    Fruit_methods.append(f)
    return f

Or, more generically:

import collections
Food_methods = collections.defaultdict(list)

def food_type(type):
    def store(f):
        Food_methods[type].append(f)
        return f
    return store

Upvotes: 1

Related Questions