3

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)
Community
  • 1
  • 1
zekel
  • 9,227
  • 10
  • 65
  • 96
  • What Python version are you on? This is much easier in 3.6 with `__set_name__` (which, contrary to the name, isn't intended to be limited to setting names). – user2357112 Apr 14 '17 at 20:33
  • Good question: I'm using 2.7.10 and looking for the best solution with that. That being said, I'm open to tempting reasons to upgrade. – zekel Apr 14 '17 at 20:35

2 Answers2

1

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
aghast
  • 14,785
  • 3
  • 24
  • 56
1

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.

iafisher
  • 938
  • 7
  • 14