jelcOfDice
jelcOfDice

Reputation: 33

Choosing one of several methods on object instantiation

I have a python class, let's call it MyClass, with several bound methods. I need the behaviour of one of these methods, call it .dynamic_method(), to be drastically different depending on the value of a parameter set during object creation. There will be a handful of variations of dynamic_method which will share very little code between them, so it makes sense to define them as separate functions/methods rather than as a single large method with many options and conditionals. I do not need to be able to change the behaviour of this method after creating the object, everything can be set during __init__().

For reference, MyClass manages data sets, and .dynamic_method deals with some IO aspects of this data which change depending on dataset and context.

This is not technically difficult to achieve, but none of the methods I've come up with feel completely correct:

  1. Define the method variations as functions separately, and attach one of them as a bound method during __init__() using one of the methods described here: Adding a Method to an Existing Object Instance. This seems like the most natural choice to me, but many of the answers in that thread strongly discourages the use of this pattern.
  2. Define all possible methods in the class definition as .dynamic_method_a, .dynamic_method_b etc, and set an option/alias so that MyClass.dynamic_method() calls the correct one. This will result in a very long class definition, and lots of unused methods for each object.
  3. Store the methods as separate functions, and have .dynamic_method() simply calls the correct function with self as the first argument. This feels like a wilful misuse of the concept of methods, and will break linting.
  4. Use a factory pattern or object generator function. I'd rather avoid this, as it will add by far the most complexity to the code base.

What would be considered the most "Pythonic" way to achieve this behaviour? One of the above methods, or is there an obvious alternative I've missed?

Upvotes: 3

Views: 87

Answers (3)

Mihai
Mihai

Reputation: 2155

I would rather go for option 3, since I find it cleaner, but it depends on how many implementation variations you need. Option 4 (use a factory) is good as well, but it will generate more code. On the other side, option 4 is by far more scalable and maintainable.

So, since you want to change the behaviour of the dynamic_method() within __init__(), you could do it like this:

class MyClass(object):
     def __init__(self, param=None):
         self.dynamic_method = {
             "a": self.dynamic_a,
             "b": self.dynamic_b,
             "c": self.dynamic_c}[param]

     def dynamic_a(self):
         print("a functionality")

     def dynamic_b(self):
         print("b functionality")

     def dynamic_c(self):
         print("c functionality")

>> m = MyClass("b")
>> m.dynamic_method()
b functionality

Upvotes: 1

Eliy Arlev
Eliy Arlev

Reputation: 578

I would suggest the factory, Indeed it adds complexity, but let me point out few advantages.

  • flexibility to future changes
  • when reading code, it is easier to understand what kind of behavior to expect from an object just by it's class
  • If you use IDE with type hinting (pycharm or other) your coding will be easier as the IDE can understand what method the object will use before run-time

code sample:

class DataStruct:

    def __init__(self, input_: str):
        self.Field = input_

class MyClass:

    def __init__(self, data: DataStruct):
        self.Data: DataStruct = data

    def dynamic_method(self):
        # Abstract Method
        pass

    def __str__(self):
        return self.Data.Field

class MyClassFunctionalityOne(MyClass):

    def __init__(self, data: DataStruct):
        super().__init__(data)

    def dynamic_method(self):
        self.Data.Field = self.Data.Field.upper()


class MyClassFunctionalityTwo(MyClass):

    def __init__(self, data: DataStruct):
        super().__init__(data)

    def dynamic_method(self):
        self.Data.Field = "and now for something completely different"


class MyClassFactory:

    def __init__(self):
        pass

    @classmethod
    def manufacture(cls, input_: DataStruct) -> MyClass:
        #replace this if..elif chain to contain the tests on the data you need for determine the right method
        if input_.Field.count('one') > 0:
            obj: MyClass = MyClassFunctionalityOne(input_)
        elif input_.Field.count('two')> 0:
            obj: MyClass = MyClassFunctionalityTwo(input_)
        else:
            obj = None

        return obj


# script starts here
received_data = DataStruct('This kind of data should result in functionality one')
object1 = MyClassFactory.manufacture(received_data)
received_data = DataStruct('This kind of data should result in functionality two')
object2 = MyClassFactory.manufacture(received_data)
print (type(object1))
print (type(object2))
print ('*'*5, 'objects before dynamic_method', '*'*5)
print (object1)
print (object2)
object1.dynamic_method()
object2.dynamic_method()
print ('*'*5, 'objects after dynamic_method', '*'*5)
print (object1)
print (object2)

output:

<class '__main__.MyClassFunctionalityOne'>
<class '__main__.MyClassFunctionalityTwo'>
 ***** objects before dynamic_method *****
This kind of data should result in functionality one
This kind of data should result in functionality two
***** objects after dynamic_method *****
THIS KIND OF DATA SHOULD RESULT IN FUNCTIONALITY ONE
and now for something completely different

Upvotes: 1

Tom Gringauz
Tom Gringauz

Reputation: 811

I think you are over-complicating this issue.

if you don't have any important reason to change the method attribute (first option), I would go with just writing a nice if-else in dynamic_method() to call the other functions (3rd option) or if I really want to be fancy I would make a makeshift switch-case out of a dict like this:

def dynamic_method(self):
    return {
        <value to run foo1>: foo1,
        <value to run foo2>: foo2,
        <value to run foo3>: foo3
    }[self.method_variation](self)

def foo1(self):
    pass

def foo2(self):
    pass

def foo3(self):
    pass

Upvotes: 3

Related Questions