Home > Software design >  Conditionally defining method of a class
Conditionally defining method of a class

Time:10-27

I have a Config class. I would like it to have several methods, which depends on another variable of the class. This way I would only have to change one variable in the config class and then it would function differently.

I tried the following. When creating an instance I can access other_param, but the objective function is lost.

class Config:
    def __init__(self):
        self.task = 'Task1'

        if self.task=='Task1':
            self.other_param = 1
            def objective(self,probs):
                return probs[0]

        if self.task=='Task2':
            self.other_param = 2
            def objective(self,probs):
                return probs[1]

In fact I can get a solution by reversing the logic, i.e. defining a method and having an if statement within that. But I want several methods for each if branch... and I don't want to have all these methods full of if statements. The code would just be more readable if all the methods belonging to the same if branch would be in the same place.

CodePudding user response:

Something like this should work for your problem:

class Config:

    def __init__(self, task: str):
        self.task = task
        if self.task == 'Task1':
            self.other_param = 1
            self.objective = lambda probs: probs[0]
        elif self.task == 'Task2':
            self.other_param = 2
            self.objective = lambda probs: probs[1]

>>> a = Config('Task1')
>>> a.objective([1,2])
1
>>> a = Config('Task2')
>>> a.objective([1,2])
2

For multi-lines functions:

class Config:

    def __init__(self, task: str):
        self.task = task
        if self.task == 'Task1':
            self.other_param = 1

            def objective(probs):
                ...
                return probs[0]

            self.objective = objective
        elif self.task == 'Task2':
            self.other_param = 2

            def objective(probs):
                ...
                return probs[1]

            self.objective = objective

Note that self is implicitly declared in the objective function and can be accessed directly in it.

CodePudding user response:

I would use simple class derivation here, with a static build method in base class.

class Config:
    # _subs = {"Task1": Config1, 'Task2': Config2} # subclasses are not defined yet
    @classmethod
    def build(cls, task):
        return cls._subs[task]()
        conf.otherparam = task[-1]   # or whatever common initialization

    
class Config1(Config):
    def objective(self, probs):
        return probs[0]

    
class Config2(Config):
    def objective(self, probs):
        return probs[1]

# set _subs static member once subclasse have been defined   
Config._subs = {"Task1": Config1, 'Task2': Config2}

You can then use

conf1 = Config.build('Task1')
conf1.objective(probs)
...

You could make the base class abstract to ensure that objective is overriden.

CodePudding user response:

I think any way of avoiding having conditionally-defined methods on the class is better than doing that.

Looking at your example code, it could be written as:

class Config:
    def __init__(self):
        self.task = 'Task1'

        self._probs_index = 0

        if self.task == 'Task1':
            self.other_param = 1

        if self.task == 'Task2':
            self.other_param = 2
            self._probs_index = 1
    
    def objective(self, probs):
        return probs[self._probs_index]

Probably your real code is more complex and you will say this won't work for you.

But I would suggest to avoid what you're currently trying to do at all costs.

It's not clear if task is an arg to __init__ in your real code (the example doesn't make much sense at the moment)

If it is we might re-write the above as:

class Config:
    def __init__(self, task):
        self.task = task

        # default values
        self._probs_index = 0
        self.other_param = 0

        if self.task == 'Task1':
            self.other_param = 1
        elif self.task == 'Task2':
            self.other_param = 2
            self._probs_index = 1
        else:
            # unexpected value, choose here if you want to
            # continue and use defaults or raise an error
            raise ValueError(f"Unexpected task: {task}")
    
    def objective(self, probs):
        return probs[self._probs_index]

If it's not then we could think of them as sub-classes. It would be natural to treat classes with related but different behaviour as sub-classes.

from abc import ABC, abstractmethod


class BaseConfig(ABC):
    task = None  # overwrite in sub-classes

    # default values, overwrite in sub-classes if needed
    other_param = 0
    
    @abstractmethod
    def objective(self, probs):
        pass


class Task1Config(BaseConfig):
    task = "Task1"
    other_param = 1

    def objective(self, probs):
        return probs[0]


class Task2Config(BaseConfig):
    task = "Task2"
    other_param = 2

    def objective(self, probs):
        return probs[1]
  • Related