5

I have a series of functions that serve to classify data. Each function is passed the same input. The goal of this system is to be able to drop in new classification functions at will without having to adjust anything.

To do this, I make use of a classes_in_module function lifted from here. Then, every classifier in one python file will be ran on each input.

However, I am finding that implementing the classifier as either a class or a function is kludgy. Classes mean instantiating and executing, while functions lack clean introspection to allow me to query the name or use inheritance to define common values.

Here is an example. First, the class implementation:

class AbstractClassifier(object):
    @property
    def name(self):
        return self.__class__.__name__

class ClassifierA(AbstractClassifier):
    def __init__(self, data):
        self.data = data
    def run(self):
        return 1

This can then be used in this fashion, assuming that classifier_list is the output of classes_in_module on a file containing ClassifierA among others:

result = []
for classifier in classifier_list:
    c = classifier(data)
    result.append(c.run())

However, this seems a bit silly. This class is obviously static, and doesn't really need to maintain its own state, as it is used once and discarded. The classifier is really a function, but then I lose the ability to have a shared name property -- I would have to use the ugly introspection technique sys._getframe().f_code.co_name and replicate that code for each classifier function. And any other shared properties between classifiers would also be lost.

What do you think? Should I just accept this mis-use of classes? Or is there a better way?

Community
  • 1
  • 1
Ian Fiddes
  • 2,821
  • 5
  • 29
  • 49
  • 1
    Do your classifier functions really need to look up their own name? – jwodder Feb 18 '16 at 21:28
  • Maybe the compromise would be to use the `@staticmethod` decorator? – nthall Feb 18 '16 at 21:30
  • @jwodder it would make things more convenient. You are right, though, that I could work around the name issue. However, there are still other shared properties in the abstract base class that I did not include in the example. – Ian Fiddes Feb 18 '16 at 21:34
  • @IanFiddes functions have a `func_name` attribute and can also contain member data (see examples below) – Jared Goguen Feb 18 '16 at 22:00

3 Answers3

2

Functions can have member data. You can also find the name of a function using the func_name attribute.

def classifier(data):
    return 1
classifier.name = classifier.func_name

print(classifier.name) #classifier

If you want multiple functions to behave the same way, you can use a decorator.

function_tracker = []

def add_attributes(function):
    function.name = function.func_name
    function.id = len(function_tracker)
    function_tracker.append(function)
    return function

@add_attributes
def classifier(data):
    return 1

print(classifier.name, classifier.id) # 'classifier', 0

Would this work to avoid classes in your specific case?

Jared Goguen
  • 8,772
  • 2
  • 18
  • 36
1

If you don't need several instances of the class (and it seems you don't) make one instance of the class and change the run to __call__:

class AbstractClassifier(object):
    @property
    def name(self):
        return self.__class__.__name__

class ClassifierA(AbstractClassifier):
    def __call__(self, data):
        return 1
ClassifierA = ClassifierA()       # see below for alternatives

and then in your other code:

result = []
for classifier in classifier_list:
    result.append(classifier(data))

Instead of having ClassifierA = ClassifierA() (which isn't very elegant), one could do:

classifier_list = [c() for c in (ClassifierA, ClassifierB, ...)]

This method allows you to keep your classes handy should you need to create more instances of them; if you don't ever need to have more than one instance you could use a decorator to IAYG (instantiate as you go ;) :

def instantiate(cls):
    return cls()

@instantiate
class ClassifierZ(object):
    def __call__(self, data):
        return some_classification
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • 1
    There's a syntax error on the last line (extra dot). – Seán Hayes Feb 18 '16 at 21:36
  • 1
    That would have to be `ClassifierA()()`, right? Once to instantiate, then a second pair of parentheses to actually call. – Mark Dickinson Feb 18 '16 at 21:36
  • @MarkDickinson: No, the classes are instantiated already either in creating the list, or immediately after creating the class. – Ethan Furman Feb 18 '16 at 21:37
  • Ah, sorry; I see what you're doing now. `ClassifierA = ClassifierA()` is pretty horrible, though. :-) – Mark Dickinson Feb 18 '16 at 21:38
  • 1
    @MarkDickinson: Yeah, that's why I added the `list` comment; alternatively, one could use an `@instantiate` decorator; it depends on what else the OP might be doing with those classes. – Ethan Furman Feb 18 '16 at 21:40
  • This is great, thanks. Could you elaborate more on a `@instantiate` decorator? I will not be doing anything else with these classes - they are one-and-done. – Ian Fiddes Feb 18 '16 at 21:41
1

To use a class instance as a function:

class ClassifierA(AbstractClassifier):
    def __init__(self, data):
        self.data = data
    def __call__(self):
        return 1

result = []
for classifier in classifier_list:
    c = classifier(data)
    result.append(c())

Or to just use functions:

classifier_list = []
def my_decorator(func):
    classifier_list.append(func)
    return func

@my_decorator
def classifier_a(data):
    return 1

result = []
for classifier in classifier_list:
    c = classifier(data)
    result.append(c)
Seán Hayes
  • 4,060
  • 4
  • 33
  • 48