Reputation: 1353
I have a class that I use for data analysis. Generally I need to create a couple instances of the class and then run a series of methods on each. The methods I need to run will vary from instance to instance.
Today my code (using the class) usually looks something like this:
object_alpha = MyClass()
object_alpha.method1()
object_alpha.method2()
object_alpha.method3(arg1)
object_alpha.method4()
object_bravo = MyClass()
object_bravo.method1()
object_bravo.method3(arg1)
object_bravo.method4()
object_bravo.method5()
I know the above example is not an example of method chaining. Method chaining is not currently possible because the methods do not return an object of the class.
This format gets a bit repetitive a tedious, especially with long descriptive variable names. My primary complaint is that I do not find it very readable.
I thought about changing my class to return a new object from each method call, so that I could do method chaining. But the side-effect of the methods do not change the class--they are making changes to a database via an API, so it feels strange to return a new object.
My thought is to create a runner class that would take a list of method names as strings. So that I could do something like this.
object_alpha = MyClass().runner([
'method1',
'method2',
'method3(arg1)',
'method4'
])
object_bravo = MyClass().runner([
'method1',
'method3(arg1)',
'method4',
'method5'
])
Is this a bad idea; is there a better approach?
Upvotes: 2
Views: 250
Reputation: 114468
Your idea is good, but it can use one major improvement: instead of using a list of strings that you have to eval, use a list of tuples or something like that to contain the methods themselves and their arguments:
object_bravo = MyClass()
bravo_runner = [
(object_bravo.method1, (arg1, arg2), {k1: v1, k2, v2}),
(object_bravo.method2, (arg3), {}),
(object_bravo.method3, (), {k3: v3}),
(MyClass.method4, (object_bravo, arg4), {k4: v4})
]
The way to run that would be much easier than parsing strings:
for spec in bravo_runner:
spec[0](*spec[1], **spec[2])
If you used namedtuple
for the runner elements, it would look even better:
from collections import namedtuple
RunnerSpec = namedtuple('RunnerSpec', ['method', 'args', 'kwargs'])
object_bravo = MyClass()
bravo_runner = [
RunnerSpec(object_bravo.method1, (arg1, arg2), {k1: v1, k2, v2}),
RunnerSpec(object_bravo.method2, (arg3), {}),
RunnerSpec(object_bravo.method3, (), {k3: v3}),
RunnerSpec(MyClass.method4, (object_bravo, arg4), {k4: v4})
]
and the run
method:
for spec in bravo_runner:
spec.method(*spec.args, **spec.kwargs)
Conclusion
At this point, you may actually save some typing by just writing a method within the class for each sequence/scenario that uses self
instead of the descriptive object name. That would save you the most in the end because you would have pre-constructed, named sequences of calls. No need to store them as lists. bravo_object.run_scenario3()
is better than bravo_object.runner([big_ass_hard_to_read_list_that_is_basically_a_method_anyway]).run()
.
Upvotes: 1
Reputation: 5323
Basically, you're trying to invent a domain-specific language here. Since its sole purpose would be to execute something much like Python, only without having to name the context object, I don't think it's worth the hassle. The pattern of returning an object from a method to allow method chaining, even though the object is not the logical result of the operation, is actually not unheard of, so I'd go for that.
Upvotes: 1